Vi-style editor and other editor improvements (#40)

* WIP VI wrapper for editor

* WIP: block cursor

* Create Edit trait, run CI on all feature options

* Add prints describing build steps to ci.sh

* Custom rendering for Vi editor

* Clippy fixes

* More clippy fixes

* Show clippy results in CI

* Fix for Redox

* Fix clippy lint

* Add vi feature to enable vi-style editor

* Add escape to libcosmic text box
This commit is contained in:
Jeremy Soller 2022-11-15 12:26:59 -07:00 committed by GitHub
parent 271ca5cf7a
commit ee54e7626b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 982 additions and 616 deletions

View file

@ -11,12 +11,12 @@ env:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
- uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --all-features
- name: Run Rust CI
run: ./ci.sh

View file

@ -1,7 +1,7 @@
[package]
name = "cosmic-text"
description = "Pure Rust multi-line text handling"
version = "0.4.1"
version = "0.5.0-alpha1"
authors = ["Jeremy Soller <jeremy@system76.com>"]
edition = "2021"
license = "MIT OR Apache-2.0"
@ -39,6 +39,7 @@ std = [
"sys-locale",
"unicode-bidi/std",
]
vi = ["syntect"]
[workspace]
members = [

32
ci.sh Executable file
View file

@ -0,0 +1,32 @@
#!/usr/bin/env bash
function build {
cargo build --release "$@"
cargo clippy --no-deps "$@"
}
set -ex
echo Build with default features
build
echo Build with no default features
build --no-default-features
echo Build with only std feature
build --no-default-features --features std
echo Build with only swash feature
build --no-default-features --features swash
echo Build with only syntect feature
build --no-default-features --features syntect
echo Build with only vi feature
build --no-default-features --features vi
echo Build with all features
build --all-features
echo Run tests
cargo test

View file

@ -23,3 +23,7 @@ version = "0.10"
#TODO: iced portal
#default-features = false
#features = ["xdg-portal"]
[features]
default = []
vi = ["cosmic-text/vi"]

View file

@ -27,6 +27,7 @@ use cosmic_text::{
Attrs,
AttrsList,
Buffer,
Edit,
FontSystem,
Metrics,
SyntaxEditor,
@ -39,9 +40,6 @@ use std::{
sync::Mutex,
};
use self::syntax_text_box::syntax_text_box;
mod syntax_text_box;
use self::text::text;
mod text;
@ -74,7 +72,10 @@ pub struct Window {
theme: Theme,
path_opt: Option<PathBuf>,
attrs: Attrs<'static>,
#[cfg(not(feature = "vi"))]
editor: Mutex<SyntaxEditor<'static>>,
#[cfg(feature = "vi")]
editor: Mutex<cosmic_text::ViEditor<'static>>,
}
#[allow(dead_code)]
@ -121,6 +122,10 @@ impl Application for Window {
&SYNTAX_SYSTEM,
"base16-eighties.dark"
).unwrap();
#[cfg(feature = "vi")]
let mut editor = cosmic_text::ViEditor::new(editor);
update_attrs(&mut editor, attrs);
let mut window = Window {
@ -180,7 +185,7 @@ impl Application for Window {
});
let mut editor = self.editor.lock().unwrap();
update_attrs(&mut editor, self.attrs);
update_attrs(&mut *editor, self.attrs);
},
Message::Italic(italic) => {
self.attrs = self.attrs.style(if italic {
@ -190,7 +195,7 @@ impl Application for Window {
});
let mut editor = self.editor.lock().unwrap();
update_attrs(&mut editor, self.attrs);
update_attrs(&mut *editor, self.attrs);
},
Message::Monospaced(monospaced) => {
self.attrs = self.attrs
@ -202,7 +207,7 @@ impl Application for Window {
.monospaced(monospaced);
let mut editor = self.editor.lock().unwrap();
update_attrs(&mut editor, self.attrs);
update_attrs(&mut *editor, self.attrs);
},
Message::MetricsChanged(metrics) => {
let mut editor = self.editor.lock().unwrap();
@ -220,7 +225,7 @@ impl Application for Window {
self.attrs = self.attrs.color(cosmic_text::Color::rgba(as_u8(r), as_u8(g), as_u8(b), as_u8(a)));
let mut editor = self.editor.lock().unwrap();
update_attrs(&mut editor, self.attrs);
update_attrs(&mut *editor, self.attrs);
},
}
@ -266,7 +271,7 @@ impl Application for Window {
.align_items(Alignment::Center)
.spacing(8)
,
syntax_text_box(&self.editor)
text_box(&self.editor)
]
.spacing(8)
.padding(16)
@ -277,7 +282,7 @@ impl Application for Window {
}
}
fn update_attrs<'a>(editor: &mut SyntaxEditor<'a>, attrs: Attrs<'a>) {
fn update_attrs<'a, T: Edit<'a>>(editor: &mut T, attrs: Attrs<'a>) {
editor.buffer_mut().lines.iter_mut().for_each(|line| {
line.set_attrs_list(AttrsList::new(attrs));
});

View file

@ -1,304 +0,0 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
use cosmic::{
iced_native::{
{Color, Element, Length, Point, Rectangle, Shell, Size},
clipboard::Clipboard,
event::{Event, Status},
image,
keyboard::{Event as KeyEvent, KeyCode},
layout::{self, Layout},
mouse::{self, Button, Event as MouseEvent, ScrollDelta},
renderer,
widget::{self, tree, Widget},
Padding
},
};
use cosmic_text::{
Action,
SyntaxEditor,
SwashCache,
};
use std::{
cmp,
sync::Mutex,
time::Instant,
};
use super::text;
pub struct SyntaxTextBox<'a> {
editor: &'a Mutex<SyntaxEditor<'static>>,
padding: Padding,
}
impl<'a> SyntaxTextBox<'a> {
pub fn new(editor: &'a Mutex<SyntaxEditor<'static>>) -> Self {
Self {
editor,
padding: Padding::new(0),
}
}
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
self.padding = padding.into();
self
}
}
pub fn syntax_text_box<'a>(editor: &'a Mutex<SyntaxEditor<'static>>) -> SyntaxTextBox<'a> {
SyntaxTextBox::new(editor)
}
impl<'a, Message, Renderer> Widget<Message, Renderer> for SyntaxTextBox<'a>
where
Renderer: renderer::Renderer + image::Renderer<Handle = image::Handle>,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>()
}
fn state(&self) -> tree::State {
tree::State::new(State::new())
}
fn width(&self) -> Length {
Length::Fill
}
fn height(&self) -> Length {
Length::Fill
}
fn layout(
&self,
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let limits = limits.width(Length::Fill).height(Length::Fill);
//TODO: allow lazy shape
let mut editor = self.editor.lock().unwrap();
editor.buffer_mut().shape_until(i32::max_value());
let mut layout_lines = 0;
for line in editor.buffer().lines.iter() {
match line.layout_opt() {
Some(layout) => layout_lines += layout.len(),
None => (),
}
}
let height = layout_lines as f32 * editor.buffer().metrics().line_height as f32;
let size = Size::new(limits.max().width, height);
log::info!("size {:?}", size);
layout::Node::new(limits.resolve(size))
}
fn mouse_interaction(
&self,
_tree: &widget::Tree,
layout: Layout<'_>,
cursor_position: Point,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
if layout.bounds().contains(cursor_position) {
mouse::Interaction::Text
} else {
mouse::Interaction::Idle
}
}
fn draw(
&self,
tree: &widget::Tree,
renderer: &mut Renderer,
_theme: &Renderer::Theme,
_style: &renderer::Style,
layout: Layout<'_>,
_cursor_position: Point,
viewport: &Rectangle,
) {
let state = tree.state.downcast_ref::<State>();
let mut editor = self.editor.lock().unwrap();
let view_w = cmp::min(viewport.width as i32, layout.bounds().width as i32) - self.padding.horizontal() as i32;
let view_h = cmp::min(viewport.height as i32, layout.bounds().height as i32)- self.padding.vertical() as i32;
editor.buffer_mut().set_size(view_w, view_h);
editor.shape_as_needed();
let instant = Instant::now();
let mut pixels = vec![0; view_w as usize * view_h as usize * 4];
editor.draw(&mut state.cache.lock().unwrap(), |x, y, w, h, color| {
if w <= 0 || h <= 0 {
// Do not draw invalid sized rectangles
return;
}
if w > 1 || h > 1 {
// Draw rectangles with optimized quad renderer
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle::new(
layout.position() + [x as f32, y as f32].into() + [self.padding.left as f32, self.padding.top as f32].into(),
Size::new(w as f32, h as f32)
),
border_radius: 0.0,
border_width: 0.0,
border_color: Color::TRANSPARENT,
},
Color::from_rgba8(
color.r(),
color.g(),
color.b(),
(color.a() as f32) / 255.0
)
);
} else {
text::draw_pixel(&mut pixels, view_w, view_h, x, y, color);
}
});
let handle = image::Handle::from_pixels(view_w as u32, view_h as u32, pixels);
image::Renderer::draw(renderer, handle, Rectangle::new(
layout.position() + [self.padding.left as f32, self.padding.top as f32].into(),
Size::new(view_w as f32, view_h as f32)
));
let duration = instant.elapsed();
log::debug!("redraw {}, {}: {:?}", view_w, view_h, duration);
}
fn on_event(
&mut self,
tree: &mut widget::Tree,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
_shell: &mut Shell<'_, Message>,
) -> Status {
let state = tree.state.downcast_mut::<State>();
let mut editor = self.editor.lock().unwrap();
let mut status = Status::Ignored;
match event {
Event::Keyboard(KeyEvent::KeyPressed { key_code, modifiers }) => match key_code {
KeyCode::Left => {
editor.action(Action::Left);
status = Status::Captured;
},
KeyCode::Right => {
editor.action(Action::Right);
status = Status::Captured;
},
KeyCode::Up => {
editor.action(Action::Up);
status = Status::Captured;
},
KeyCode::Down => {
editor.action(Action::Down);
status = Status::Captured;
},
KeyCode::Home => {
editor.action(Action::Home);
status = Status::Captured;
},
KeyCode::End => {
editor.action(Action::End);
status = Status::Captured;
},
KeyCode::PageUp => {
editor.action(Action::PageUp);
status = Status::Captured;
},
KeyCode::PageDown => {
editor.action(Action::PageDown);
status = Status::Captured;
},
KeyCode::Enter => {
editor.action(Action::Enter);
status = Status::Captured;
},
KeyCode::Backspace => {
editor.action(Action::Backspace);
status = Status::Captured;
},
KeyCode::Delete => {
editor.action(Action::Delete);
status = Status::Captured;
},
_ => ()
},
Event::Keyboard(KeyEvent::CharacterReceived(character)) => {
editor.action(Action::Insert(character));
status = Status::Captured;
},
Event::Mouse(MouseEvent::ButtonPressed(Button::Left)) => {
if layout.bounds().contains(cursor_position) {
editor.action(Action::Click {
x: (cursor_position.x - layout.bounds().x) as i32 - self.padding.left as i32,
y: (cursor_position.y - layout.bounds().y) as i32 - self.padding.top as i32,
});
state.is_dragging = true;
status = Status::Captured;
}
},
Event::Mouse(MouseEvent::ButtonReleased(Button::Left)) => {
state.is_dragging = false;
status = Status::Captured;
},
Event::Mouse(MouseEvent::CursorMoved { .. }) => {
if state.is_dragging {
editor.action(Action::Drag {
x: (cursor_position.x - layout.bounds().x) as i32 - self.padding.left as i32,
y: (cursor_position.y - layout.bounds().y) as i32 - self.padding.top as i32,
});
status = Status::Captured;
}
},
Event::Mouse(MouseEvent::WheelScrolled { delta }) => match delta {
ScrollDelta::Lines { x, y } => {
editor.action(Action::Scroll {
lines: (-y * 6.0) as i32,
});
status = Status::Captured;
},
_ => (),
},
_ => ()
}
status
}
}
impl<'a, Message, Renderer> From<SyntaxTextBox<'a>> for Element<'a, Message, Renderer>
where
Renderer: renderer::Renderer + image::Renderer<Handle = image::Handle>,
{
fn from(text_box: SyntaxTextBox<'a>) -> Self {
Self::new(text_box)
}
}
pub struct State {
is_dragging: bool,
cache: Mutex<SwashCache<'static>>,
}
impl State {
/// Creates a new [`State`].
pub fn new() -> State {
State {
is_dragging: false,
cache: Mutex::new(SwashCache::new(&crate::FONT_SYSTEM)),
}
}
}

View file

@ -17,7 +17,7 @@ use cosmic::{
};
use cosmic_text::{
Action,
Editor,
Edit,
SwashCache,
};
use std::{
@ -51,34 +51,35 @@ impl StyleSheet for Theme {
}
}
pub struct TextBox<'a> {
editor: &'a Mutex<Editor<'static>>,
pub struct TextBox<'a, Editor> {
editor: &'a Mutex<Editor>,
padding: Padding,
}
impl<'a> TextBox<'a> {
pub fn new(editor: &'a Mutex<Editor<'static>>) -> Self {
impl<'a, Editor> TextBox<'a, Editor> {
pub fn new(editor: &'a Mutex<Editor>) -> Self {
Self {
editor,
padding: Padding::new(0),
}
}
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
self.padding = padding.into();
self
}
}
pub fn text_box<'a>(editor: &'a Mutex<Editor<'static>>) -> TextBox<'a> {
pub fn text_box<'a, Editor>(editor: &'a Mutex<Editor>) -> TextBox<'a, Editor> {
TextBox::new(editor)
}
impl<'a, Message, Renderer> Widget<Message, Renderer> for TextBox<'a>
impl<'a, 'editor, Editor, Message, Renderer> Widget<Message, Renderer> for TextBox<'a, Editor>
where
Renderer: renderer::Renderer + image::Renderer<Handle = image::Handle>,
Renderer::Theme: StyleSheet,
Editor: Edit<'editor>,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>()
@ -105,17 +106,17 @@ where
//TODO: allow lazy shape
let mut editor = self.editor.lock().unwrap();
editor.buffer.shape_until(i32::max_value());
editor.buffer_mut().shape_until(i32::max_value());
let mut layout_lines = 0;
for line in editor.buffer.lines.iter() {
for line in editor.buffer().lines.iter() {
match line.layout_opt() {
Some(layout) => layout_lines += layout.len(),
None => (),
}
}
let height = layout_lines as f32 * editor.buffer.metrics().line_height as f32;
let height = layout_lines as f32 * editor.buffer().metrics().line_height as f32;
let size = Size::new(limits.max().width, height);
log::info!("size {:?}", size);
@ -174,7 +175,7 @@ where
let view_w = cmp::min(viewport.width as i32, layout.bounds().width as i32) - self.padding.horizontal() as i32;
let view_h = cmp::min(viewport.height as i32, layout.bounds().height as i32) - self.padding.vertical() as i32;
editor.buffer.set_size(view_w, view_h);
editor.buffer_mut().set_size(view_w, view_h);
editor.shape_as_needed();
@ -270,6 +271,10 @@ where
editor.action(Action::PageDown);
status = Status::Captured;
},
KeyCode::Escape => {
editor.action(Action::Escape);
status = Status::Captured;
},
KeyCode::Enter => {
editor.action(Action::Enter);
status = Status::Captured;
@ -327,12 +332,13 @@ where
}
}
impl<'a, Message, Renderer> From<TextBox<'a>> for Element<'a, Message, Renderer>
impl<'a, 'editor, Editor, Message, Renderer> From<TextBox<'a, Editor>> for Element<'a, Message, Renderer>
where
Renderer: renderer::Renderer + image::Renderer<Handle = image::Handle>,
Renderer::Theme: StyleSheet,
Editor: Edit<'editor>,
{
fn from(text_box: TextBox<'a>) -> Self {
fn from(text_box: TextBox<'a, Editor>) -> Self {
Self::new(text_box)
}
}

View file

@ -12,3 +12,8 @@ env_logger = "0.9"
fontdb = "0.9"
log = "0.4"
orbclient = "0.3.35"
unicode-segmentation = "1.7"
[features]
default = []
vi = ["cosmic-text/vi"]

View file

@ -4,6 +4,7 @@ use cosmic_text::{
Action,
Attrs,
Buffer,
Edit,
Family,
FontSystem,
Metrics,
@ -60,12 +61,16 @@ fn main() {
let mut font_size_i = font_size_default;
let line_x = 8 * display_scale;
let mut editor = SyntaxEditor::new(
Buffer::new(&font_system, font_sizes[font_size_i]),
&syntax_system,
"base16-eighties.dark"
).unwrap();
#[cfg(feature = "vi")]
let mut editor = cosmic_text::ViEditor::new(editor);
editor.buffer_mut().set_size(
window.width() as i32 - line_x * 2,
window.height() as i32
@ -89,13 +94,14 @@ fn main() {
let mut mouse_left = false;
loop {
editor.shape_as_needed();
if editor.buffer_mut().redraw {
if editor.buffer().redraw() {
let instant = Instant::now();
let bg = editor.background_color();
window.set(orbclient::Color::rgb(bg.r(), bg.g(), bg.b()));
editor.draw(&mut swash_cache, |x, y, w, h, color| {
let fg = editor.foreground_color();
editor.draw(&mut swash_cache, fg, |x, y, w, h, color| {
window.rect(line_x + x, y, w, h, orbclient::Color { data: color.0 })
});
@ -127,7 +133,7 @@ fn main() {
window.sync();
editor.buffer_mut().redraw = false;
editor.buffer_mut().set_redraw(false);
log::debug!("redraw: {:?}", instant.elapsed());
}
@ -148,6 +154,7 @@ fn main() {
orbclient::K_END if event.pressed => editor.action(Action::End),
orbclient::K_PGUP if event.pressed => editor.action(Action::PageUp),
orbclient::K_PGDN if event.pressed => editor.action(Action::PageDown),
orbclient::K_ESC if event.pressed => editor.action(Action::Escape),
orbclient::K_ENTER if event.pressed => editor.action(Action::Enter),
orbclient::K_BKSP if event.pressed => editor.action(Action::Backspace),
orbclient::K_DEL if event.pressed => editor.action(Action::Delete),

View file

@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
use cosmic_text::{Action, Buffer, Color, Editor, FontSystem, Metrics, SwashCache};
use cosmic_text::{Action, Buffer, Color, Edit, Editor, FontSystem, Metrics, SwashCache};
use orbclient::{EventOption, Renderer, Window, WindowFlag};
use std::{env, fs, process, time::Instant};
use unicode_segmentation::UnicodeSegmentation;
@ -10,7 +10,7 @@ fn redraw(window: &mut Window, editor: &mut Editor<'_>, swash_cache: &mut SwashC
let font_color = Color::rgb(0xFF, 0xFF, 0xFF);
editor.shape_as_needed();
if editor.buffer.redraw {
if editor.buffer().redraw() {
let instant = Instant::now();
window.set(bg_color);
@ -21,7 +21,7 @@ fn redraw(window: &mut Window, editor: &mut Editor<'_>, swash_cache: &mut SwashC
window.sync();
editor.buffer.redraw = false;
editor.buffer_mut().set_redraw(false);
let duration = instant.elapsed();
log::debug!("redraw: {:?}", duration);
@ -146,7 +146,7 @@ fn main() {
let mut wrong = 0;
for (line_i, line) in text.lines().enumerate() {
let buffer_line = &editor.buffer.lines[line_i];
let buffer_line = &editor.buffer().lines[line_i];
if buffer_line.text() != line {
log::error!("line {}: {:?} != {:?}", line_i, buffer_line.text(), line);
wrong += 1;

View file

@ -7,6 +7,7 @@ use cosmic_text::{
Buffer,
BufferLine,
Color,
Edit,
Editor,
Family,
FontSystem,
@ -49,7 +50,7 @@ fn main() {
Metrics::new(32, 44).scale(display_scale)
));
editor.buffer.set_size(
editor.buffer_mut().set_size(
window.width() as i32,
window.height() as i32
);
@ -59,7 +60,7 @@ fn main() {
let mono_attrs = attrs.monospaced(true).family(Family::Monospace);
let comic_attrs = attrs.family(Family::Name("Comic Neue"));
editor.buffer.lines.clear();
editor.buffer_mut().lines.clear();
let lines: &[&[(&str, Attrs)]] = &[
&[
@ -131,7 +132,7 @@ fn main() {
let end = line_text.len();
attrs_list.add_span(start..end, attrs);
}
editor.buffer.lines.push(BufferLine::new(line_text, attrs_list));
editor.buffer_mut().lines.push(BufferLine::new(line_text, attrs_list));
}
let mut swash_cache = SwashCache::new(&font_system);
@ -145,7 +146,7 @@ fn main() {
let font_color = Color::rgb(0xFF, 0xFF, 0xFF);
editor.shape_as_needed();
if editor.buffer.redraw {
if editor.buffer().redraw() {
let instant = Instant::now();
window.set(bg_color);
@ -156,7 +157,7 @@ fn main() {
window.sync();
editor.buffer.redraw = false;
editor.buffer_mut().set_redraw(false);
let duration = instant.elapsed();
log::debug!("redraw: {:?}", duration);
@ -193,7 +194,7 @@ fn main() {
}
},
EventOption::Resize(resize) => {
editor.buffer.set_size(resize.width as i32, resize.height as i32);
editor.buffer_mut().set_size(resize.width as i32, resize.height as i32);
},
EventOption::Quit(_) => process::exit(0),
_ => (),

View file

@ -35,25 +35,25 @@ impl Color {
/// Get the red component
#[inline]
pub fn r(&self) -> u8 {
((self.0 & 0x00FF0000) >> 16) as u8
((self.0 & 0x00_FF_00_00) >> 16) as u8
}
/// Get the green component
#[inline]
pub fn g(&self) -> u8 {
((self.0 & 0x0000FF00) >> 8) as u8
((self.0 & 0x00_00_FF_00) >> 8) as u8
}
/// Get the blue component
#[inline]
pub fn b(&self) -> u8 {
(self.0 & 0x000000FF) as u8
(self.0 & 0x00_00_00_FF) as u8
}
/// Get the alpha component
#[inline]
pub fn a(&self) -> u8 {
((self.0 & 0xFF000000) >> 24) as u8
((self.0 & 0xFF_00_00_00) >> 24) as u8
}
}
@ -81,7 +81,7 @@ impl FamilyOwned {
pub fn as_family(&self) -> Family {
match self {
FamilyOwned::Name(name) => Family::Name(&name),
FamilyOwned::Name(name) => Family::Name(name),
FamilyOwned::Serif => Family::Serif,
FamilyOwned::SansSerif => Family::SansSerif,
FamilyOwned::Cursive => Family::Cursive,
@ -278,7 +278,7 @@ impl AttrsList {
}
for (key, resize) in removes {
let (range, attrs) = self.spans.get_key_value(&key.start).map(|v| (v.0.clone(), v.1.clone())).unwrap();
let (range, attrs) = self.spans.get_key_value(&key.start).map(|v| (v.0.clone(), v.1.clone())).expect("attrs span not found");
self.spans.remove(key);
if resize {

View file

@ -57,7 +57,7 @@ pub struct LayoutRun<'a> {
pub line_y: i32,
}
/// An iterator of visible text lines, see [LayoutRun]
/// An iterator of visible text lines, see [`LayoutRun`]
pub struct LayoutRunIter<'a, 'b> {
buffer: &'b Buffer<'a>,
line_i: usize,
@ -145,8 +145,7 @@ impl fmt::Display for Metrics {
/// A buffer of text that is shaped and laid out
pub struct Buffer<'a> {
/// The [FontSystem] used by this [Buffer]
pub font_system: &'a FontSystem,
font_system: &'a FontSystem,
/// [BufferLine]s (or paragraphs) of text in the buffer
pub lines: Vec<BufferLine>,
metrics: Metrics,
@ -154,11 +153,11 @@ pub struct Buffer<'a> {
height: i32,
scroll: i32,
/// True if a redraw is requires. Set to false after processing
pub redraw: bool,
redraw: bool,
}
impl<'a> Buffer<'a> {
/// Create a new [Buffer] with the provided [FontSystem] and [Metrics]
/// Create a new [`Buffer`] with the provided [`FontSystem`] and [`Metrics`]
pub fn new(
font_system: &'a FontSystem,
metrics: Metrics,
@ -180,7 +179,7 @@ impl<'a> Buffer<'a> {
#[cfg(feature = "std")]
let instant = std::time::Instant::now();
for line in self.lines.iter_mut() {
for line in &mut self.lines {
if line.shape_opt().is_some() {
line.reset_layout();
line.layout(
@ -204,7 +203,7 @@ impl<'a> Buffer<'a> {
let mut reshaped = 0;
let mut total_layout = 0;
for line in self.lines.iter_mut() {
for line in &mut self.lines {
if total_layout >= lines {
break;
}
@ -293,7 +292,8 @@ impl<'a> Buffer<'a> {
pub fn layout_cursor(&self, cursor: &Cursor) -> LayoutCursor {
let line = &self.lines[cursor.line];
let layout = line.layout_opt().as_ref().unwrap(); //TODO: ensure layout is done?
//TODO: ensure layout is done?
let layout = line.layout_opt().as_ref().expect("layout not found");
for (layout_i, layout_line) in layout.iter().enumerate() {
for (glyph_i, glyph) in layout_line.glyphs.iter().enumerate() {
if cursor.index == glyph.start {
@ -333,24 +333,29 @@ impl<'a> Buffer<'a> {
)
}
/// Get [`FontSystem`] used by this [`Buffer`]
pub fn font_system(&self) -> &'a FontSystem {
self.font_system
}
/// Shape the provided line index and return the result
pub fn line_shape(&mut self, line_i: usize) -> Option<&ShapeLine> {
let line = self.lines.get_mut(line_i)?;
Some(line.shape(&self.font_system))
Some(line.shape(self.font_system))
}
/// Lay out the provided line index and return the result
pub fn line_layout(&mut self, line_i: usize) -> Option<&[LayoutLine]> {
let line = self.lines.get_mut(line_i)?;
Some(line.layout(&self.font_system, self.metrics.font_size, self.width))
Some(line.layout(self.font_system, self.metrics.font_size, self.width))
}
/// Get the current [Metrics]
/// Get the current [`Metrics`]
pub fn metrics(&self) -> Metrics {
self.metrics
}
/// Set the current [Metrics]
/// Set the current [`Metrics`]
pub fn set_metrics(&mut self, metrics: Metrics) {
if metrics != self.metrics {
self.metrics = metrics;
@ -408,6 +413,16 @@ impl<'a> Buffer<'a> {
self.shape_until_scroll();
}
/// True if a redraw is needed
pub fn redraw(&self) -> bool {
self.redraw
}
/// Set redraw needed flag
pub fn set_redraw(&mut self, redraw: bool) {
self.redraw = redraw;
}
/// Get the visible layout runs for rendering and other tasks
pub fn layout_runs<'b>(&'b self) -> LayoutRunIter<'a, 'b> {
LayoutRunIter::new(self)
@ -439,15 +454,15 @@ impl<'a> Buffer<'a> {
let mut new_cursor_char = 0;
let mut first_glyph = true;
'hit: for (glyph_i, glyph) in run.glyphs.iter().enumerate() {
if first_glyph {
if first_glyph {
first_glyph = false;
if (run.rtl && x > glyph.x as i32) || (!run.rtl && x < 0) {
new_cursor_glyph = 0;
new_cursor_char = 0;
}
}
}
if x >= glyph.x as i32
&& x <= (glyph.x + glyph.w) as i32
{
@ -528,7 +543,7 @@ impl<'a> Buffer<'a> {
};
cache.with_pixels(cache_key, glyph_color, |x, y, color| {
f(x_int + x, run.line_y + y_int + y, 1, 1, color)
f(x_int + x, run.line_y + y_int + y, 1, 1, color);
});
}
}

View file

@ -18,8 +18,8 @@ pub struct BufferLine {
impl BufferLine {
/// Create a new line with the given text and attributes list
/// Cached shaping and layout can be done using the [Self::shape] and
/// [Self::layout] functions
/// Cached shaping and layout can be done using the [`Self::shape`] and
/// [`Self::layout`] functions
pub fn new<T: Into<String>>(text: T, attrs_list: AttrsList) -> Self {
Self {
text: text.into(),
@ -40,7 +40,7 @@ impl BufferLine {
/// Will reset shape and layout if it differs from current text and attributes list.
/// Returns true if the line was reset
pub fn set_text<T: AsRef<str> + Into<String>>(&mut self, text: T, attrs_list: AttrsList) -> bool {
if text.as_ref() != &self.text || attrs_list != self.attrs_list {
if text.as_ref() != self.text || attrs_list != self.attrs_list {
self.text = text.into();
self.attrs_list = attrs_list;
self.reset();
@ -143,7 +143,7 @@ impl BufferLine {
self.shape_opt = Some(ShapeLine::new(font_system, &self.text, &self.attrs_list));
self.layout_opt = None;
}
self.shape_opt.as_ref().unwrap()
self.shape_opt.as_ref().expect("shape not found")
}
/// Get line shaping cache
@ -163,7 +163,7 @@ impl BufferLine {
);
self.layout_opt = Some(layout);
}
self.layout_opt.as_ref().unwrap()
self.layout_opt.as_ref().expect("layout not found")
}
/// Get line layout cache

View file

@ -5,52 +5,13 @@ use alloc::string::{String, ToString};
use core::cmp;
use unicode_segmentation::UnicodeSegmentation;
use crate::{AttrsList, Buffer, BufferLine, Cursor, LayoutCursor};
use crate::{Action, AttrsList, Buffer, BufferLine, Cursor, Edit, LayoutCursor};
#[cfg(feature = "swash")]
use crate::Color;
/// An action to perform on an [Editor]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Action {
/// Move cursor to previous character ([Self::Left] in LTR, [Self::Right] in RTL)
Previous,
/// Move cursor to next character ([Self::Right] in LTR, [Self::Left] in RTL)
Next,
/// Move cursor left
Left,
/// Move cursor right
Right,
/// Move cursor up
Up,
/// Move cursor down
Down,
/// Move cursor to start of line
Home,
/// Move cursor to end of line
End,
/// Scroll up one page
PageUp,
/// Scroll down one page
PageDown,
/// Insert character at cursor
Insert(char),
/// Create new line
Enter,
/// Delete text behind cursor
Backspace,
/// Delete text in front of cursor
Delete,
/// Mouse click at specified position
Click { x: i32, y: i32 },
/// Mouse drag to specified position
Drag { x: i32, y: i32 },
/// Scroll specified number of lines
Scroll { lines: i32 },
}
/// A wrapper of [Buffer] for easy editing
/// A wrapper of [`Buffer`] for easy editing
pub struct Editor<'a> {
pub buffer: Buffer<'a>,
buffer: Buffer<'a>,
cursor: Cursor,
cursor_x_opt: Option<i32>,
select_opt: Option<Cursor>,
@ -58,7 +19,7 @@ pub struct Editor<'a> {
}
impl<'a> Editor<'a> {
/// Create a new [Editor] with the provided [Buffer]
/// Create a new [`Editor`] with the provided [`Buffer`]
pub fn new(buffer: Buffer<'a>) -> Self {
Self {
buffer,
@ -69,18 +30,8 @@ impl<'a> Editor<'a> {
}
}
/// Shape lines until scroll, after adjusting scroll if the cursor moved
pub fn shape_as_needed(&mut self) {
if self.cursor_moved {
self.buffer.shape_until_cursor(self.cursor);
self.cursor_moved = false;
} else {
self.buffer.shape_until_scroll();
}
}
fn set_layout_cursor(&mut self, cursor: LayoutCursor) {
let layout = self.buffer.line_layout(cursor.line).unwrap();
let layout = self.buffer.line_layout(cursor.line).expect("layout not found");
let layout_line = match layout.get(cursor.layout) {
Some(some) => some,
@ -102,27 +53,38 @@ impl<'a> Editor<'a> {
if self.cursor.line != cursor.line || self.cursor.index != new_index {
self.cursor.line = cursor.line;
self.cursor.index = new_index;
self.buffer.redraw = true;
self.buffer.set_redraw(true);
}
}
}
/// Get the internal [Buffer]
pub fn buffer(&self) -> &Buffer<'a> {
impl<'a> Edit<'a> for Editor<'a> {
fn buffer(&self) -> &Buffer<'a> {
&self.buffer
}
/// Get the internal [Buffer], mutably
pub fn buffer_mut(&mut self) -> &mut Buffer<'a> {
fn buffer_mut(&mut self) -> &mut Buffer<'a> {
&mut self.buffer
}
/// Get the current cursor position
pub fn cursor(&self) -> Cursor {
fn cursor(&self) -> Cursor {
self.cursor
}
/// Copy selection
pub fn copy_selection(&mut self) -> Option<String> {
fn select_opt(&self) -> Option<Cursor> {
self.select_opt
}
fn shape_as_needed(&mut self) {
if self.cursor_moved {
self.buffer.shape_until_cursor(self.cursor);
self.cursor_moved = false;
} else {
self.buffer.shape_until_scroll();
}
}
fn copy_selection(&mut self) -> Option<String> {
let select = self.select_opt?;
let (start, end) = match select.line.cmp(&self.cursor.line) {
@ -166,9 +128,7 @@ impl<'a> Editor<'a> {
Some(selection)
}
/// Delete selection, adjusting cursor and returning true if there was a selection
// Helper function for backspace, delete, insert, and enter when there is a selection
pub fn delete_selection(&mut self) -> bool {
fn delete_selection(&mut self) -> bool {
let select = match self.select_opt.take() {
Some(some) => some,
None => return false,
@ -235,8 +195,7 @@ impl<'a> Editor<'a> {
true
}
/// Perform an [Action] on the editor
pub fn action(&mut self, action: Action) {
fn action(&mut self, action: Action) {
let old_cursor = self.cursor;
match action {
@ -254,11 +213,11 @@ impl<'a> Editor<'a> {
}
self.cursor.index = prev_index;
self.buffer.redraw = true;
self.buffer.set_redraw(true);
} else if self.cursor.line > 0 {
self.cursor.line -= 1;
self.cursor.index = self.buffer.lines[self.cursor.line].text().len();
self.buffer.redraw = true;
self.buffer.set_redraw(true);
}
self.cursor_x_opt = None;
},
@ -268,14 +227,14 @@ impl<'a> Editor<'a> {
for (i, c) in line.text().grapheme_indices(true) {
if i == self.cursor.index {
self.cursor.index += c.len();
self.buffer.redraw = true;
self.buffer.set_redraw(true);
break;
}
}
} else if self.cursor.line + 1 < self.buffer.lines.len() {
self.cursor.line += 1;
self.cursor.index = 0;
self.buffer.redraw = true;
self.buffer.set_redraw(true);
}
self.cursor_x_opt = None;
},
@ -326,7 +285,7 @@ impl<'a> Editor<'a> {
//TODO: make this preserve X as best as possible!
let mut cursor = self.buffer.layout_cursor(&self.cursor);
let layout_len = self.buffer.line_layout(cursor.line).unwrap().len();
let layout_len = self.buffer.line_layout(cursor.line).expect("layout not found").len();
if self.cursor_x_opt.is_none() {
self.cursor_x_opt = Some(
@ -371,6 +330,11 @@ impl<'a> Editor<'a> {
scroll += self.buffer.visible_lines();
self.buffer.set_scroll(scroll);
},
Action::Escape => {
if self.select_opt.take().is_some() {
self.buffer.set_redraw(true);
}
},
Action::Insert(character) => {
if character.is_control()
&& !['\t', '\u{92}'].contains(&character)
@ -484,20 +448,20 @@ impl<'a> Editor<'a> {
if let Some(new_cursor) = self.buffer.hit(x, y) {
if new_cursor != self.cursor {
self.cursor = new_cursor;
self.buffer.redraw = true;
self.buffer.set_redraw(true);
}
}
},
Action::Drag { x, y } => {
if self.select_opt.is_none() {
self.select_opt = Some(self.cursor);
self.buffer.redraw = true;
self.buffer.set_redraw(true);
}
if let Some(new_cursor) = self.buffer.hit(x, y) {
if new_cursor != self.cursor {
self.cursor = new_cursor;
self.buffer.redraw = true;
self.buffer.set_redraw(true);
}
}
},
@ -531,7 +495,7 @@ impl<'a> Editor<'a> {
/// Draw the editor
#[cfg(feature = "swash")]
pub fn draw<F>(&self, cache: &mut crate::SwashCache, color: Color, mut f: F)
fn draw<F>(&self, cache: &mut crate::SwashCache, color: Color, mut f: F)
where F: FnMut(i32, i32, u32, u32, Color)
{
let font_size = self.buffer.metrics().font_size;
@ -699,7 +663,7 @@ impl<'a> Editor<'a> {
};
cache.with_pixels(cache_key, glyph_color, |x, y, color| {
f(x_int + x, line_y + y_int + y, 1, 1, color)
f(x_int + x, line_y + y_int + y, 1, 1, color);
});
}
}

93
src/edit/mod.rs Normal file
View file

@ -0,0 +1,93 @@
#[cfg(not(feature = "std"))]
use alloc::string::String;
use crate::{Buffer, Cursor};
#[cfg(feature = "swash")]
use crate::Color;
pub use self::editor::*;
mod editor;
#[cfg(feature = "syntect")]
pub use self::syntect::*;
#[cfg(feature = "syntect")]
mod syntect;
#[cfg(feature = "vi")]
pub use self::vi::*;
#[cfg(feature = "vi")]
mod vi;
/// An action to perform on an [`Editor`]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Action {
/// Move cursor to previous character ([Self::Left] in LTR, [Self::Right] in RTL)
Previous,
/// Move cursor to next character ([Self::Right] in LTR, [Self::Left] in RTL)
Next,
/// Move cursor left
Left,
/// Move cursor right
Right,
/// Move cursor up
Up,
/// Move cursor down
Down,
/// Move cursor to start of line
Home,
/// Move cursor to end of line
End,
/// Scroll up one page
PageUp,
/// Scroll down one page
PageDown,
/// Escape, clears selection
Escape,
/// Insert character at cursor
Insert(char),
/// Create new line
Enter,
/// Delete text behind cursor
Backspace,
/// Delete text in front of cursor
Delete,
/// Mouse click at specified position
Click { x: i32, y: i32 },
/// Mouse drag to specified position
Drag { x: i32, y: i32 },
/// Scroll specified number of lines
Scroll { lines: i32 },
}
/// A trait to allow easy replacements of [`Editor`], like `SyntaxEditor`
pub trait Edit<'a> {
/// Get the internal [`Buffer`]
fn buffer(&self) -> &Buffer<'a>;
/// Get the internal [`Buffer`], mutably
fn buffer_mut(&mut self) -> &mut Buffer<'a>;
/// Get the current cursor position
fn cursor(&self) -> Cursor;
/// Get the current selection position
fn select_opt(&self) -> Option<Cursor>;
/// Shape lines until scroll, after adjusting scroll if the cursor moved
fn shape_as_needed(&mut self);
/// Copy selection
fn copy_selection(&mut self) -> Option<String>;
/// Delete selection, adjusting cursor and returning true if there was a selection
// Also used by backspace, delete, insert, and enter when there is a selection
fn delete_selection(&mut self) -> bool;
/// Perform an [Action] on the editor
fn action(&mut self, action: Action);
/// Draw the editor
#[cfg(feature = "swash")]
fn draw<F>(&self, cache: &mut crate::SwashCache, color: Color, f: F)
where F: FnMut(i32, i32, u32, u32, Color);
}

View file

@ -30,6 +30,7 @@ use crate::{
Buffer,
Color,
Cursor,
Edit,
Editor,
Style,
Weight,
@ -41,7 +42,7 @@ pub struct SyntaxSystem {
}
impl SyntaxSystem {
/// Create a new [SyntaxSystem]
/// Create a new [`SyntaxSystem`]
pub fn new() -> Self {
Self {
//TODO: store newlines in buffer
@ -51,7 +52,7 @@ impl SyntaxSystem {
}
}
/// A wrapper of [Editor] with syntax highlighting provided by [SyntaxSystem]
/// A wrapper of [`Editor`] with syntax highlighting provided by [`SyntaxSystem`]
pub struct SyntaxEditor<'a> {
editor: Editor<'a>,
syntax_system: &'a SyntaxSystem,
@ -62,7 +63,7 @@ pub struct SyntaxEditor<'a> {
}
impl<'a> SyntaxEditor<'a> {
/// Create a new [SyntaxEditor] with the provided [Buffer], [SyntaxSystem], and theme name.
/// Create a new [`SyntaxEditor`] with the provided [`Buffer`], [`SyntaxSystem`], and theme name.
///
/// A good default theme name is "base16-eighties.dark".
///
@ -84,12 +85,16 @@ impl<'a> SyntaxEditor<'a> {
}
/// Load text from a file, and also set syntax to the best option
///
/// ## Errors
///
/// Returns an [`io::Error`] if reading the file fails
#[cfg(feature = "std")]
pub fn load_text<P: AsRef<Path>>(&mut self, path: P, attrs: crate::Attrs<'a>) -> io::Result<()> {
let path = path.as_ref();
let text = fs::read_to_string(path)?;
self.editor.buffer.set_text(&text, attrs);
self.editor.buffer_mut().set_text(&text, attrs);
//TODO: re-use text
self.syntax = match self.syntax_system.syntax_set.find_syntax_for_file(path) {
@ -110,14 +115,61 @@ impl<'a> SyntaxEditor<'a> {
Ok(())
}
/// Shape as needed, also doing syntax highlighting
pub fn shape_as_needed(&mut self) {
/// Get the default background color
pub fn background_color(&self) -> Color {
if let Some(background) = self.theme.settings.background {
Color::rgba(
background.r,
background.g,
background.b,
background.a,
)
} else {
Color::rgb(0, 0, 0)
}
}
/// Get the default foreground (text) color
pub fn foreground_color(&self) -> Color {
if let Some(foreground) = self.theme.settings.foreground {
Color::rgba(
foreground.r,
foreground.g,
foreground.b,
foreground.a,
)
} else {
Color::rgb(0xFF, 0xFF, 0xFF)
}
}
}
impl<'a> Edit<'a> for SyntaxEditor<'a> {
fn buffer(&self) -> &Buffer<'a> {
self.editor.buffer()
}
fn buffer_mut(&mut self) -> &mut Buffer<'a> {
self.editor.buffer_mut()
}
fn cursor(&self) -> Cursor {
self.editor.cursor()
}
fn select_opt(&self) -> Option<Cursor> {
self.editor.select_opt()
}
fn shape_as_needed(&mut self) {
#[cfg(feature = "std")]
let now = std::time::Instant::now();
let buffer = self.editor.buffer_mut();
let mut highlighted = 0;
for line_i in 0..self.editor.buffer.lines.len() {
let line = &mut self.editor.buffer.lines[line_i];
for line_i in 0..buffer.lines.len() {
let line = &mut buffer.lines[line_i];
if ! line.is_reset() && line_i < self.syntax_cache.len() {
continue;
}
@ -132,7 +184,7 @@ impl<'a> SyntaxEditor<'a> {
)
};
let ops = parse_state.parse_line(line.text(), &self.syntax_system.syntax_set).unwrap();
let ops = parse_state.parse_line(line.text(), &self.syntax_system.syntax_set).expect("failed to parse syntax");
let ranges = RangedHighlightIterator::new(
&mut highlight_state,
&ops,
@ -172,14 +224,14 @@ impl<'a> SyntaxEditor<'a> {
line.set_wrap_simple(true);
//TODO: efficiently do syntax highlighting without having to shape whole buffer
line.shape(self.editor.buffer.font_system);
buffer.line_shape(line_i);
let cache_item = (parse_state.clone(), highlight_state.clone());
if line_i < self.syntax_cache.len() {
if self.syntax_cache[line_i] != cache_item {
self.syntax_cache[line_i] = cache_item;
if line_i + 1 < self.editor.buffer.lines.len() {
self.editor.buffer.lines[line_i + 1].reset();
if line_i + 1 < buffer.lines.len() {
buffer.lines[line_i + 1].reset();
}
}
} else {
@ -188,7 +240,7 @@ impl<'a> SyntaxEditor<'a> {
}
if highlighted > 0 {
self.editor.buffer.redraw = true;
buffer.set_redraw(true);
#[cfg(feature = "std")]
log::debug!("Syntax highlighted {} lines in {:?}", highlighted, now.elapsed());
}
@ -196,67 +248,21 @@ impl<'a> SyntaxEditor<'a> {
self.editor.shape_as_needed();
}
/// Get the internal [Buffer]
pub fn buffer(&self) -> &Buffer<'a> {
&self.editor.buffer
}
/// Get the internal [Buffer], mutably
pub fn buffer_mut(&mut self) -> &mut Buffer<'a> {
&mut self.editor.buffer
}
/// Get the current [Cursor] position
pub fn cursor(&self) -> Cursor {
self.editor.cursor()
}
/// Get the default background color
pub fn background_color(&self) -> Color {
if let Some(background) = self.theme.settings.background {
Color::rgba(
background.r,
background.g,
background.b,
background.a,
)
} else {
Color::rgb(0, 0, 0)
}
}
/// Get the default foreground (text) color
pub fn foreground_color(&self) -> Color {
if let Some(foreground) = self.theme.settings.foreground {
Color::rgba(
foreground.r,
foreground.g,
foreground.b,
foreground.a,
)
} else {
Color::rgb(0xFF, 0xFF, 0xFF)
}
}
/// Copy selection
pub fn copy_selection(&mut self) -> Option<String> {
fn copy_selection(&mut self) -> Option<String> {
self.editor.copy_selection()
}
/// Delete selection, adjusting cursor and returning true if there was a selection
pub fn delete_selection(&mut self) -> bool {
fn delete_selection(&mut self) -> bool {
self.editor.delete_selection()
}
/// Perform an [Action] on the editor
pub fn action(&mut self, action: Action) {
fn action(&mut self, action: Action) {
self.editor.action(action);
}
/// Draw the editor
#[cfg(feature = "swash")]
pub fn draw<F>(&self, cache: &mut crate::SwashCache, mut f: F)
fn draw<F>(&self, cache: &mut crate::SwashCache, _color: Color, mut f: F)
where F: FnMut(i32, i32, u32, u32, Color)
{
let size = self.buffer().size();

393
src/edit/vi.rs Normal file
View file

@ -0,0 +1,393 @@
use alloc::string::String;
use core::cmp;
use unicode_segmentation::UnicodeSegmentation;
use crate::{
Action,
Buffer,
Color,
Cursor,
Edit,
SyntaxEditor,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Mode {
Normal,
Insert,
Command,
Search,
SearchBackwards,
}
pub struct ViEditor<'a> {
editor: SyntaxEditor<'a>,
mode: Mode,
}
impl<'a> ViEditor<'a> {
pub fn new(editor: SyntaxEditor<'a>) -> Self {
Self {
editor,
mode: Mode::Normal,
}
}
/// Load text from a file, and also set syntax to the best option
#[cfg(feature = "std")]
pub fn load_text<P: AsRef<std::path::Path>>(&mut self, path: P, attrs: crate::Attrs<'a>) -> std::io::Result<()> {
self.editor.load_text(path, attrs)
}
/// Get the default background color
pub fn background_color(&self) -> Color {
self.editor.background_color()
}
/// Get the default foreground (text) color
pub fn foreground_color(&self) -> Color {
self.editor.foreground_color()
}
}
impl<'a> Edit<'a> for ViEditor<'a> {
fn buffer(&self) -> &Buffer<'a> {
self.editor.buffer()
}
fn buffer_mut(&mut self) -> &mut Buffer<'a> {
self.editor.buffer_mut()
}
fn cursor(&self) -> Cursor {
self.editor.cursor()
}
fn select_opt(&self) -> Option<Cursor> {
self.editor.select_opt()
}
fn shape_as_needed(&mut self) {
self.editor.shape_as_needed()
}
fn copy_selection(&mut self) -> Option<String> {
self.editor.copy_selection()
}
fn delete_selection(&mut self) -> bool {
self.editor.delete_selection()
}
fn action(&mut self, action: Action) {
let old_mode = self.mode;
match self.mode {
Mode::Normal => match action {
Action::Insert(c) => match c {
// Enter insert mode after cursor
'a' => {
self.editor.action(Action::Right);
self.mode = Mode::Insert;
},
// Enter insert mode at end of line
'A' => {
self.editor.action(Action::End);
self.mode = Mode::Insert;
},
// Enter insert mode at cursor
'i' => {
self.mode = Mode::Insert;
},
// Enter insert mode at start of line
'I' => {
//TODO: soft home, skip whitespace
self.editor.action(Action::Home);
self.mode = Mode::Insert;
}
// Create line after and enter insert mode
'o' => {
self.editor.action(Action::End);
self.editor.action(Action::Enter);
self.mode = Mode::Insert;
},
// Create line before and enter insert mode
'O' => {
self.editor.action(Action::Home);
self.editor.action(Action::Enter);
self.editor.shape_as_needed(); // TODO: do not require this?
self.editor.action(Action::Up);
self.mode = Mode::Insert;
},
// Left
'h' => self.editor.action(Action::Left),
// Top of screen
//TODO: 'H' => self.editor.action(Action::ScreenHigh),
// Down
'j' => self.editor.action(Action::Down),
// Up
'k' => self.editor.action(Action::Up),
// Right
'l' => self.editor.action(Action::Right),
// Bottom of screen
//TODO: 'L' => self.editor.action(Action::ScreenLow),
// Middle of screen
//TODO: 'M' => self.editor.action(Action::ScreenMiddle),
// Remove character at cursor
'x' => self.editor.action(Action::Delete),
// Remove character before cursor
'X' => self.editor.action(Action::Backspace),
// Go to start of line
'0' => self.editor.action(Action::Home),
// Go to end of line
'$' => self.editor.action(Action::End),
// Go to start of line after whitespace
//TODO: implement this
'^' => self.editor.action(Action::Home),
// Enter command mode
':' => {
self.mode = Mode::Command;
},
// Enter search mode
'/' => {
self.mode = Mode::Search;
},
// Enter search backwards mode
'?' => {
self.mode = Mode::SearchBackwards;
},
_ => (),
},
_ => self.editor.action(action),
},
Mode::Insert => match action {
Action::Escape => {
let cursor = self.cursor();
let layout_cursor = self.buffer().layout_cursor(&cursor);
if layout_cursor.glyph > 0 {
self.editor.action(Action::Left);
}
self.mode = Mode::Normal;
},
_ => self.editor.action(action),
},
_ => {
//TODO: other modes
self.mode = Mode::Normal;
},
}
if self.mode != old_mode {
self.buffer_mut().set_redraw(true);
}
}
#[cfg(feature = "swash")]
fn draw<F>(&self, cache: &mut crate::SwashCache, color: Color, mut f: F)
where F: FnMut(i32, i32, u32, u32, Color)
{
let font_size = self.buffer().metrics().font_size;
let line_height = self.buffer().metrics().line_height;
for run in self.buffer().layout_runs() {
let line_i = run.line_i;
let line_y = run.line_y;
let cursor_glyph_opt = |cursor: &Cursor| -> Option<(usize, f32, f32)> {
//TODO: better calculation of width
let default_width = (font_size as f32) / 2.0;
if cursor.line == line_i {
for (glyph_i, glyph) in run.glyphs.iter().enumerate() {
if cursor.index >= glyph.start && cursor.index < glyph.end {
// Guess x offset based on characters
let mut before = 0;
let mut total = 0;
let cluster = &run.text[glyph.start..glyph.end];
for (i, _) in cluster.grapheme_indices(true) {
if glyph.start + i < cursor.index {
before += 1;
}
total += 1;
}
let width = glyph.w / (total as f32);
let offset = (before as f32) * width;
return Some((glyph_i, offset, width));
}
}
match run.glyphs.last() {
Some(glyph) => {
if cursor.index == glyph.end {
return Some((run.glyphs.len(), 0.0, default_width));
}
},
None => {
return Some((0, 0.0, default_width));
}
}
}
None
};
// Highlight selection (TODO: HIGHLIGHT COLOR!)
if let Some(select) = self.select_opt() {
let (start, end) = match select.line.cmp(&self.cursor().line) {
cmp::Ordering::Greater => (self.cursor(), select),
cmp::Ordering::Less => (select, self.cursor()),
cmp::Ordering::Equal => {
/* select.line == self.cursor.line */
if select.index < self.cursor().index {
(select, self.cursor())
} else {
/* select.index >= self.cursor.index */
(self.cursor(), select)
}
}
};
if line_i >= start.line && line_i <= end.line {
let mut range_opt = None;
for glyph in run.glyphs.iter() {
// Guess x offset based on characters
let cluster = &run.text[glyph.start..glyph.end];
let total = cluster.grapheme_indices(true).count();
let mut c_x = glyph.x;
let c_w = glyph.w / total as f32;
for (i, c) in cluster.grapheme_indices(true) {
let c_start = glyph.start + i;
let c_end = glyph.start + i + c.len();
if (start.line != line_i || c_end > start.index)
&& (end.line != line_i || c_start < end.index) {
range_opt = match range_opt.take() {
Some((min, max)) => Some((
cmp::min(min, c_x as i32),
cmp::max(max, (c_x + c_w) as i32),
)),
None => Some((
c_x as i32,
(c_x + c_w) as i32,
))
};
} else if let Some((min, max)) = range_opt.take() {
f(
min,
line_y - font_size,
cmp::max(0, max - min) as u32,
line_height as u32,
Color::rgba(color.r(), color.g(), color.b(), 0x33)
);
}
c_x += c_w;
}
}
if run.glyphs.is_empty() && end.line > line_i{
// Highlight all of internal empty lines
range_opt = Some((0, self.buffer().size().0));
}
if let Some((mut min, mut max)) = range_opt.take() {
if end.line > line_i {
// Draw to end of line
if run.rtl {
min = 0;
} else {
max = self.buffer().size().0;
}
}
f(
min,
line_y - font_size,
cmp::max(0, max - min) as u32,
line_height as u32,
Color::rgba(color.r(), color.g(), color.b(), 0x33)
);
}
}
}
// Draw cursor
if let Some((cursor_glyph, cursor_glyph_offset, cursor_glyph_width)) = cursor_glyph_opt(&self.cursor()) {
let block_cursor = match self.mode {
Mode::Normal => true,
Mode::Insert => false,
_ => true /*TODO: determine block cursor in other modes*/
};
let (start_x, end_x) = match run.glyphs.get(cursor_glyph) {
Some(glyph) => {
// Start of detected glyph
if glyph.rtl {
(
(glyph.x + glyph.w - cursor_glyph_offset) as i32,
(glyph.x + glyph.w - cursor_glyph_offset - cursor_glyph_width) as i32,
)
} else {
(
(glyph.x + cursor_glyph_offset) as i32,
(glyph.x + cursor_glyph_offset + cursor_glyph_width) as i32
)
}
},
None => match run.glyphs.last() {
Some(glyph) => {
// End of last glyph
if glyph.rtl {
(
glyph.x as i32,
(glyph.x - cursor_glyph_width) as i32
)
} else {
(
(glyph.x + glyph.w) as i32,
(glyph.x + glyph.w + cursor_glyph_width) as i32
)
}
},
None => {
// Start of empty line
(
0,
cursor_glyph_width as i32
)
}
}
};
if block_cursor {
let left_x = cmp::min(start_x, end_x);
let right_x = cmp::max(start_x, end_x);
f(
left_x,
line_y - font_size,
(right_x - left_x) as u32,
line_height as u32,
Color::rgba(color.r(), color.g(), color.b(), 0x33),
);
} else {
f(
start_x,
line_y - font_size,
1,
line_height as u32,
color,
);
}
}
for glyph in run.glyphs.iter() {
let (cache_key, x_int, y_int) = (glyph.cache_key, glyph.x_int, glyph.y_int);
let glyph_color = match glyph.color_opt {
Some(some) => some,
None => color,
};
cache.with_pixels(cache_key, glyph_color, |x, y, color| {
f(x_int + x, line_y + y_int + y, 1, 1, color)
});
}
}
}
}

View file

@ -36,7 +36,7 @@ fn han_unification(locale: &str) -> &'static [&'static str] {
}
// Fallbacks to use per script
pub fn script_fallback(script: &Script, locale: &str) -> &'static [&'static str] {
pub fn script_fallback(script: Script, locale: &str) -> &'static [&'static str] {
//TODO: abstract style (sans/serif/monospaced)
//TODO: pull more data from about:config font.name-list.sans-serif in Firefox
match script {

View file

@ -108,7 +108,7 @@ impl<'a> Iterator for FontFallbackIter<'a> {
while self.script_i.0 < self.scripts.len() {
let script = self.scripts[self.script_i.0];
let script_families = script_fallback(&script, self.locale);
let script_families = script_fallback(script, self.locale);
while self.script_i.1 < script_families.len() {
let script_family = script_families[self.script_i.1];
self.script_i.1 += 1;
@ -133,7 +133,7 @@ impl<'a> Iterator for FontFallbackIter<'a> {
return Some(font);
}
}
log::warn!("failed to find family '{}'", common_family)
log::warn!("failed to find family '{}'", common_family);
}
//TODO: do we need to do this?

View file

@ -13,6 +13,6 @@ pub fn forbidden_fallback() -> &'static [&'static str] {
}
// Fallbacks to use per script
pub fn script_fallback(script: &Script, locale: &str) -> &'static [&'static str] {
pub fn script_fallback(_script: Script, _locale: &str) -> &'static [&'static str] {
&[]
}

View file

@ -44,7 +44,7 @@ fn han_unification(locale: &str) -> &'static [&'static str] {
}
// Fallbacks to use per script
pub fn script_fallback(script: &Script, locale: &str) -> &'static [&'static str] {
pub fn script_fallback(script: Script, locale: &str) -> &'static [&'static str] {
//TODO: abstract style (sans/serif/monospaced)
match script {
Script::Adlam => &["Noto Sans Adlam", "Noto Sans Adlam Unjoined"],

View file

@ -36,7 +36,7 @@ fn han_unification(locale: &str) -> &'static [&'static str] {
}
// Fallbacks to use per script
pub fn script_fallback(script: &Script, locale: &str) -> &'static [&'static str] {
pub fn script_fallback(script: Script, locale: &str) -> &'static [&'static str] {
//TODO: better match https://github.com/chromium/chromium/blob/master/third_party/blink/renderer/platform/fonts/win/font_fallback_win.cc#L99
match script {
Script::Adlam => &["Ebrima"],

View file

@ -1,46 +0,0 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
use core::ops::Deref;
pub struct Font<'a> {
pub info: &'a fontdb::FaceInfo,
pub data: &'a [u8],
pub rustybuzz: rustybuzz::Face<'a>,
#[cfg(feature = "swash")]
pub swash: (u32, swash::CacheKey),
}
impl<'a> Font<'a> {
pub fn new(info: &'a fontdb::FaceInfo) -> Option<Self> {
let data = match &info.source {
fontdb::Source::Binary(data) => data.deref().as_ref(),
#[cfg(feature = "std")]
fontdb::Source::File(path) => {
log::warn!("Unsupported fontdb Source::File('{}')", path.display());
return None;
}
#[cfg(feature = "std")]
fontdb::Source::SharedFile(_path, data) => data.deref().as_ref(),
};
Some(Self {
info,
data,
rustybuzz: rustybuzz::Face::from_slice(data, info.index)?,
#[cfg(feature = "swash")]
swash: {
let swash = swash::FontRef::from_index(data, info.index as usize)?;
(swash.offset, swash.key)
},
})
}
#[cfg(feature = "swash")]
pub fn as_swash(&self) -> swash::FontRef {
swash::FontRef {
data: self.data,
offset: self.swash.0,
key: self.swash.1,
}
}
}

View file

@ -1,12 +1,54 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pub(crate) mod fallback;
use core::ops::Deref;
pub(crate) use self::font::*;
mod font;
pub(crate) mod fallback;
pub use self::matches::*;
mod matches;
pub use self::system::*;
mod system;
pub struct Font<'a> {
pub info: &'a fontdb::FaceInfo,
pub data: &'a [u8],
pub rustybuzz: rustybuzz::Face<'a>,
#[cfg(feature = "swash")]
pub swash: (u32, swash::CacheKey),
}
impl<'a> Font<'a> {
pub fn new(info: &'a fontdb::FaceInfo) -> Option<Self> {
let data = match &info.source {
fontdb::Source::Binary(data) => data.deref().as_ref(),
#[cfg(feature = "std")]
fontdb::Source::File(path) => {
log::warn!("Unsupported fontdb Source::File('{}')", path.display());
return None;
}
#[cfg(feature = "std")]
fontdb::Source::SharedFile(_path, data) => data.deref().as_ref(),
};
Some(Self {
info,
data,
rustybuzz: rustybuzz::Face::from_slice(data, info.index)?,
#[cfg(feature = "swash")]
swash: {
let swash = swash::FontRef::from_index(data, info.index as usize)?;
(swash.offset, swash.key)
},
})
}
#[cfg(feature = "swash")]
pub fn as_swash(&self) -> swash::FontRef {
swash::FontRef {
data: self.data,
offset: self.swash.0,
key: self.swash.1,
}
}
}

View file

@ -3,7 +3,13 @@ pub use self::no_std::*;
#[cfg(not(feature = "std"))]
mod no_std;
#[cfg(feature = "std")]
//TODO: use std implementation on Redox
#[cfg(all(feature = "std", target_os = "redox"))]
pub use self::redox::*;
#[cfg(all(feature = "std", target_os = "redox"))]
mod redox;
#[cfg(all(feature = "std", not(target_os = "redox")))]
pub use self::std::*;
#[cfg(feature = "std")]
#[cfg(all(feature = "std", not(target_os = "redox")))]
mod std;

View file

@ -42,6 +42,8 @@ impl FontSystem {
&self.db
}
// Clippy false positive
#[allow(clippy::needless_lifetimes)]
pub fn get_font<'a>(&'a self, id: fontdb::ID) -> Option<Arc<Font<'a>>> {
let face = self.db.face(id)?;
match Font::new(face) {
@ -60,9 +62,8 @@ impl FontSystem {
continue;
}
match self.get_font(face.id) {
Some(font) => fonts.push(font),
None => (),
if let Some(font) = self.get_font(face.id) {
fonts.push(font);
}
}

104
src/font/system/redox.rs Normal file
View file

@ -0,0 +1,104 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
use alloc::{
string::{String, ToString},
sync::Arc,
vec::Vec,
};
use crate::{Attrs, Font, FontMatches};
/// Access system fonts
pub struct FontSystem{
locale: String,
db: fontdb::Database,
}
impl FontSystem {
pub fn new() -> Self {
let locale = sys_locale::get_locale().unwrap_or_else(|| {
log::warn!("failed to get system locale, falling back to en-US");
String::from("en-US")
});
log::info!("Locale: {}", locale);
//TODO: allow loading fonts from memory
let mut db = fontdb::Database::new();
{
let now = std::time::Instant::now();
db.load_fonts_dir("/ui/fonts");
//TODO: configurable default fonts
db.set_monospace_family("Fira Mono");
db.set_sans_serif_family("Fira Sans");
db.set_serif_family("DejaVu Serif");
log::info!(
"Parsed {} font faces in {}ms.",
db.len(),
now.elapsed().as_millis()
);
}
{
let now = std::time::Instant::now();
//TODO only do this on demand!
for i in 0..db.faces().len() {
let id = db.faces()[i].id;
unsafe { db.make_shared_face_data(id); }
}
log::info!(
"Mapped {} font faces in {}ms.",
db.len(),
now.elapsed().as_millis()
);
}
Self {
locale,
db,
}
}
pub fn locale(&self) -> &str {
&self.locale
}
pub fn db(&self) -> &fontdb::Database {
&self.db
}
// Clippy false positive
#[allow(clippy::needless_lifetimes)]
pub fn get_font<'a>(&'a self, id: fontdb::ID) -> Option<Arc<Font<'a>>> {
let face = self.db.face(id)?;
match Font::new(face) {
Some(font) => Some(Arc::new(font)),
None => {
log::warn!("failed to load font '{}'", face.post_script_name);
None
}
}
}
pub fn get_font_matches<'a>(&'a self, attrs: Attrs) -> Arc<FontMatches<'a>> {
let mut fonts = Vec::new();
for face in self.db.faces() {
if !attrs.matches(face) {
continue;
}
if let Some(font) = self.get_font(face.id) {
fonts.push(font);
}
}
Arc::new(FontMatches {
locale: &self.locale,
default_family: self.db.family_name(&attrs.family).to_string(),
fonts
})
}
}

View file

@ -23,6 +23,7 @@ struct FontSystemInner {
pub struct FontSystem(FontSystemInner);
impl FontSystem {
/// Create a new [`FontSystem`], that allows access to any installed system fonts
pub fn new() -> Self {
let locale = sys_locale::get_locale().unwrap_or_else(|| {
log::warn!("failed to get system locale, falling back to en-US");
@ -51,8 +52,7 @@ impl FontSystem {
let now = std::time::Instant::now();
//TODO only do this on demand!
assert_eq!(db.len(), db.faces().len());
for i in 0..db.len() {
for i in 0..db.faces().len() {
let id = db.faces()[i].id;
unsafe { db.make_shared_face_data(id); }
}
@ -80,6 +80,8 @@ impl FontSystem {
self.0.borrow_db()
}
// Clippy false positive
#[allow(clippy::needless_lifetimes)]
pub fn get_font<'a>(&'a self, id: fontdb::ID) -> Option<Arc<Font<'a>>> {
self.0.with(|fields| {
get_font(&fields, id)
@ -88,7 +90,7 @@ impl FontSystem {
pub fn get_font_matches<'a>(&'a self, attrs: Attrs) -> Arc<FontMatches<'a>> {
self.0.with(|fields| {
let mut font_matches_cache = fields.font_matches_cache.lock().unwrap();
let mut font_matches_cache = fields.font_matches_cache.lock().expect("failed to lock font matches cache");
//TODO: do not create AttrsOwned unless entry does not already exist
font_matches_cache.entry(AttrsOwned::new(attrs)).or_insert_with(|| {
let now = std::time::Instant::now();
@ -99,9 +101,8 @@ impl FontSystem {
continue;
}
match get_font(&fields, face.id) {
Some(font) => fonts.push(font),
None => (),
if let Some(font) = get_font(&fields, face.id) {
fonts.push(font);
}
}
@ -121,7 +122,7 @@ impl FontSystem {
}
fn get_font<'b>(fields: &ouroboros_impl_font_system_inner::BorrowedFields<'_, 'b>, id: fontdb::ID) -> Option<Arc<Font<'b>>> {
fields.font_cache.lock().unwrap().entry(id).or_insert_with(|| {
fields.font_cache.lock().expect("failed to lock font cache").entry(id).or_insert_with(|| {
let face = fields.db.face(id)?;
match Font::new(face) {
Some(font) => Some(Arc::new(font)),

View file

@ -7,8 +7,8 @@
//! rustybuzz, font discovery utilizes fontdb, and the rasterization is optional and utilizes
//! swash. The other features are developed internal to this library.
//!
//! It is recommended that you start by creating a [FontSystem], after which you can create a
//! [Buffer], provide it with some text, and then inspect the layout it produces. At this
//! It is recommended that you start by creating a [`FontSystem`], after which you can create a
//! [`Buffer`], provide it with some text, and then inspect the layout it produces. At this
//! point, you can use the `SwashCache` to rasterize glyphs into either images or pixels.
//!
//! ```
@ -54,6 +54,41 @@
//! });
//! ```
// Not interested in these lints
#![allow(clippy::new_without_default)]
// TODO: address ocurrances and then deny
//
// Indexing a slice can cause panics and that is something we always want to avoid
#![allow(clippy::indexing_slicing)]
// Overflows can produce unpredictable results and are only checked in debug builds
#![allow(clippy::integer_arithmetic)]
// Soundness issues
//
// Dereferencing unalinged pointers may be undefined behavior
#![deny(clippy::cast_ptr_alignment)]
// Avoid panicking in without information about the panic. Use expect
#![deny(clippy::unwrap_used)]
// This is usually a serious issue - a missing import of a define where it is interpreted
// as a catch-all variable in a match, for example
#![deny(unreachable_patterns)]
// Ensure that all must_use results are used
#![deny(unused_must_use)]
// Style issues
//
// Documentation not ideal
#![warn(clippy::doc_markdown)]
// Document possible errors
#![warn(clippy::missing_errors_doc)]
// Document possible panics
#![warn(clippy::missing_panics_doc)]
// Ensure semicolons are present
#![warn(clippy::semicolon_if_nothing_returned)]
// Ensure numbers are readable
#![warn(clippy::unreadable_literal)]
#![cfg_attr(not(feature = "std"), no_std)]
extern crate alloc;
@ -70,8 +105,8 @@ mod buffer_line;
pub use self::cache::*;
mod cache;
pub use self::editor::*;
mod editor;
pub use self::edit::*;
mod edit;
pub use self::font::*;
mod font;
@ -86,8 +121,3 @@ mod shape;
pub use self::swash::*;
#[cfg(feature = "swash")]
mod swash;
#[cfg(feature = "syntect")]
pub use self::syntect::*;
#[cfg(feature = "syntect")]
mod syntect;

View file

@ -30,11 +30,7 @@ fn shape_fallback(
buffer.push_str(run);
buffer.guess_segment_properties();
let rtl = match buffer.direction() {
rustybuzz::Direction::RightToLeft => true,
//TODO: other directions?
_ => false,
};
let rtl = matches!(buffer.direction(), rustybuzz::Direction::RightToLeft);
assert_eq!(rtl, span_rtl);
let glyph_buffer = rustybuzz::shape(&font.rustybuzz, &[], buffer);
@ -64,7 +60,7 @@ fn shape_fallback(
x_offset,
y_offset,
font_id: font.info.id,
glyph_id: info.glyph_id.try_into().unwrap(),
glyph_id: info.glyph_id.try_into().expect("failed to cast glyph ID"),
color_opt: None,
});
}
@ -96,7 +92,7 @@ fn shape_fallback(
// Set color
//TODO: these attributes should not be related to shaping
for glyph in glyphs.iter_mut() {
for glyph in &mut glyphs {
let attrs = attrs_list.get_span(glyph.start);
glyph.color_opt = attrs.color_opt;
}
@ -145,7 +141,7 @@ fn shape_run<'a>(
);
let (mut glyphs, mut missing) = shape_fallback(
font_iter.next().unwrap(),
font_iter.next().expect("no default font found"),
line,
attrs_list,
start_run,
@ -401,7 +397,7 @@ impl ShapeSpan {
// Reverse glyphs in RTL lines
if line_rtl {
for word in words.iter_mut() {
for word in &mut words {
word.glyphs.reverse();
}
}
@ -493,7 +489,7 @@ impl ShapeLine {
let end_x = if self.rtl { 0.0 } else { line_width as f32 };
let mut x = start_x;
let mut y = 0.0;
for span in self.spans.iter() {
for span in &self.spans {
//TODO: improve performance!
let mut word_ranges = Vec::new();
if wrap_simple {
@ -505,7 +501,7 @@ impl ShapeLine {
let word = &span.words[i];
let mut word_size = 0.0;
for glyph in word.glyphs.iter() {
for glyph in &word.glyphs {
word_size += font_size as f32 * glyph.x_advance;
}
@ -553,7 +549,7 @@ impl ShapeLine {
let word = &span.words[i];
let mut word_size = 0.0;
for glyph in word.glyphs.iter() {
for glyph in &word.glyphs {
word_size += font_size as f32 * glyph.x_advance;
}
@ -583,7 +579,7 @@ impl ShapeLine {
for (range, wrap) in word_ranges {
for word in span.words[range].iter() {
let mut word_size = 0.0;
for glyph in word.glyphs.iter() {
for glyph in &word.glyphs {
word_size += font_size as f32 * glyph.x_advance;
}
@ -605,7 +601,7 @@ impl ShapeLine {
y = 0.0;
}
for glyph in word.glyphs.iter() {
for glyph in &word.glyphs {
let x_advance = font_size as f32 * glyph.x_advance;
let y_advance = font_size as f32 * glyph.y_advance;
@ -630,7 +626,7 @@ impl ShapeLine {
}
if self.rtl {
x -= x_advance
x -= x_advance;
}
glyphs.push(glyph.layout(font_size, x, y, span.rtl));

View file

@ -12,7 +12,7 @@ use crate::{CacheKey, Color, FontSystem};
pub use swash::scale::image::{Content as SwashContent, Image as SwashImage};
fn swash_image<'a>(font_system: &'a FontSystem, context: &mut ScaleContext, cache_key: CacheKey) -> Option<SwashImage> {
fn swash_image(font_system: &FontSystem, context: &mut ScaleContext, cache_key: CacheKey) -> Option<SwashImage> {
let font = match font_system.get_font(cache_key.font_id) {
Some(some) => some,
None => {
@ -61,7 +61,7 @@ impl<'a> SwashCache<'a> {
/// Create a new swash cache
pub fn new(font_system: &'a FontSystem) -> Self {
Self {
font_system: font_system,
font_system,
context: ScaleContext::new(),
image_cache: Map::new()
}
@ -101,7 +101,7 @@ impl<'a> SwashCache<'a> {
y + off_y,
Color(
((image.data[i] as u32) << 24) |
base.0 & 0xFFFFFF
base.0 & 0xFF_FF_FF
)
);
i += 1;

16
test.sh
View file

@ -2,13 +2,17 @@
set -ex
echo Run CI script
./ci.sh
echo Build documentation
cargo doc
cargo test
cargo build --release --no-default-features
cargo build --release --no-default-features --features std
cargo build --release --no-default-features --features swash
cargo build --release --no-default-features --features syntect
cargo build --release --all-features
echo Build all examples
cargo build --release --all
echo Run terminal example
target/release/terminal
echo Run editor-test example
env RUST_LOG=editor_test=info target/release/editor-test