From 92cad6fe130dda74cafbe7bcc533ee05268f1b51 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Mon, 31 Oct 2022 11:24:36 -0600 Subject: [PATCH] Decouple editing from buffer --- examples/editor-libcosmic/src/main.rs | 71 +-- examples/editor-libcosmic/src/text_box.rs | 65 +- examples/editor-libcosmic/src/text_new.rs | 12 +- examples/editor-orbclient/src/main.rs | 104 ++-- examples/editor-test/src/main.rs | 78 ++- examples/rich-text/src/main.rs | 74 ++- examples/terminal/src/main.rs | 18 +- src/buffer.rs | 715 +++------------------- src/buffer_line.rs | 4 +- src/editor.rs | 595 ++++++++++++++++++ src/lib.rs | 23 +- 11 files changed, 901 insertions(+), 858 deletions(-) create mode 100644 src/editor.rs diff --git a/examples/editor-libcosmic/src/main.rs b/examples/editor-libcosmic/src/main.rs index f5646da..073ffbe 100644 --- a/examples/editor-libcosmic/src/main.rs +++ b/examples/editor-libcosmic/src/main.rs @@ -26,9 +26,10 @@ use cosmic::{ use cosmic_text::{ Attrs, AttrsList, + Buffer, + Editor, FontSystem, - TextBuffer, - TextMetrics, + Metrics, }; use std::{ env, @@ -47,13 +48,13 @@ lazy_static::lazy_static! { static ref FONT_SYSTEM: FontSystem<'static> = FontSystem::new(); } -static FONT_SIZES: &'static [TextMetrics] = &[ - TextMetrics::new(10, 14), // Caption - TextMetrics::new(14, 20), // Body - TextMetrics::new(20, 28), // Title 4 - TextMetrics::new(24, 32), // Title 3 - TextMetrics::new(28, 36), // Title 2 - TextMetrics::new(32, 44), // Title 1 +static FONT_SIZES: &'static [Metrics] = &[ + Metrics::new(10, 14), // Caption + Metrics::new(14, 20), // Body + Metrics::new(20, 28), // Title 4 + Metrics::new(24, 32), // Title 3 + Metrics::new(28, 36), // Title 2 + Metrics::new(32, 44), // Title 1 ]; fn main() -> cosmic::iced::Result { @@ -68,7 +69,7 @@ pub struct Window { theme: Theme, path_opt: Option, attrs: Attrs<'static>, - buffer: Mutex>, + editor: Mutex>, } #[allow(dead_code)] @@ -79,22 +80,22 @@ pub enum Message { Bold(bool), Italic(bool), Monospaced(bool), - MetricsChanged(TextMetrics), + MetricsChanged(Metrics), ThemeChanged(&'static str), } impl Window { pub fn open(&mut self, path: PathBuf) { - let mut buffer = self.buffer.lock().unwrap(); + let mut editor = self.editor.lock().unwrap(); match fs::read_to_string(&path) { Ok(text) => { log::info!("opened '{}'", path.display()); - buffer.set_text(&text, self.attrs); + editor.buffer.set_text(&text, self.attrs); self.path_opt = Some(path); }, Err(err) => { log::error!("failed to open '{}': {}", path.display(), err); - buffer.set_text("", self.attrs); + editor.buffer.set_text("", self.attrs); self.path_opt = None; } } @@ -112,17 +113,17 @@ impl Application for Window { .monospaced(true) .family(cosmic_text::Family::Monospace); - let mut buffer = TextBuffer::new( + let mut editor = Editor::new(Buffer::new( &FONT_SYSTEM, FONT_SIZES[1 /* Body */], - ); - update_attrs(&mut buffer, attrs); + )); + update_attrs(&mut editor, attrs); let mut window = Window { theme: Theme::Dark, path_opt: None, attrs, - buffer: Mutex::new(buffer), + editor: Mutex::new(editor), }; if let Some(arg) = env::args().nth(1) { window.open(PathBuf::from(arg)); @@ -151,9 +152,9 @@ impl Application for Window { }, Message::Save => { if let Some(path) = &self.path_opt { - let buffer = self.buffer.lock().unwrap(); + let editor = self.editor.lock().unwrap(); let mut text = String::new(); - for line in buffer.lines.iter() { + for line in editor.buffer.lines.iter() { text.push_str(line.text()); text.push('\n'); } @@ -174,8 +175,8 @@ impl Application for Window { cosmic_text::Weight::NORMAL }); - let mut buffer = self.buffer.lock().unwrap(); - update_attrs(&mut buffer, self.attrs); + let mut editor = self.editor.lock().unwrap(); + update_attrs(&mut editor, self.attrs); }, Message::Italic(italic) => { self.attrs = self.attrs.style(if italic { @@ -184,8 +185,8 @@ impl Application for Window { cosmic_text::Style::Normal }); - let mut buffer = self.buffer.lock().unwrap(); - update_attrs(&mut buffer, self.attrs); + let mut editor = self.editor.lock().unwrap(); + update_attrs(&mut editor, self.attrs); }, Message::Monospaced(monospaced) => { self.attrs = self.attrs @@ -196,12 +197,12 @@ impl Application for Window { }) .monospaced(monospaced); - let mut buffer = self.buffer.lock().unwrap(); - update_attrs(&mut buffer, self.attrs); + let mut editor = self.editor.lock().unwrap(); + update_attrs(&mut editor, self.attrs); }, Message::MetricsChanged(metrics) => { - let mut buffer = self.buffer.lock().unwrap(); - buffer.set_metrics(metrics); + let mut editor = self.editor.lock().unwrap(); + editor.buffer.set_metrics(metrics); }, Message::ThemeChanged(theme) => { self.theme = match theme { @@ -214,8 +215,8 @@ impl Application for Window { let as_u8 = |component: f32| (component * 255.0) as u8; self.attrs = self.attrs.color(cosmic_text::Color::rgba(as_u8(r), as_u8(g), as_u8(b), as_u8(a))); - let mut buffer = self.buffer.lock().unwrap(); - update_attrs(&mut buffer, self.attrs); + let mut editor = self.editor.lock().unwrap(); + update_attrs(&mut editor, self.attrs); }, } @@ -234,10 +235,10 @@ impl Application for Window { ); let font_size_picker = { - let buffer = self.buffer.lock().unwrap(); + let editor = self.editor.lock().unwrap(); pick_list( FONT_SIZES, - Some(buffer.metrics()), + Some(editor.buffer.metrics()), Message::MetricsChanged ) }; @@ -261,7 +262,7 @@ impl Application for Window { .align_items(Alignment::Center) .spacing(8) , - text_box(&self.buffer) + text_box(&self.editor) ] .spacing(8) .padding(16) @@ -272,8 +273,8 @@ impl Application for Window { } } -fn update_attrs<'a>(buffer: &mut TextBuffer<'a>, attrs: Attrs<'a>) { - buffer.lines.iter_mut().for_each(|line| { +fn update_attrs<'a>(editor: &mut Editor<'a>, attrs: Attrs<'a>) { + editor.buffer.lines.iter_mut().for_each(|line| { line.set_attrs_list(AttrsList::new(attrs)); }); } diff --git a/examples/editor-libcosmic/src/text_box.rs b/examples/editor-libcosmic/src/text_box.rs index 944a515..5774e4c 100644 --- a/examples/editor-libcosmic/src/text_box.rs +++ b/examples/editor-libcosmic/src/text_box.rs @@ -12,9 +12,10 @@ use cosmic::iced_native::{ widget::{self, tree, Widget}, }; use cosmic_text::{ + Action, + Buffer, + Editor, SwashCache, - TextAction, - TextBuffer, }; use std::{ cmp, @@ -47,19 +48,19 @@ impl StyleSheet for Theme { } pub struct TextBox<'a> { - buffer: &'a Mutex>, + editor: &'a Mutex>, } impl<'a> TextBox<'a> { - pub fn new(buffer: &'a Mutex>) -> Self { + pub fn new(editor: &'a Mutex>) -> Self { Self { - buffer, + editor, } } } -pub fn text_box<'a>(buffer: &'a Mutex>) -> TextBox<'a> { - TextBox::new(buffer) +pub fn text_box<'a>(editor: &'a Mutex>) -> TextBox<'a> { + TextBox::new(editor) } impl<'a, Message, Renderer> Widget for TextBox<'a> @@ -141,28 +142,22 @@ where let mut pixels_opt = state.pixels_opt.lock().unwrap(); - let mut buffer = self.buffer.lock().unwrap(); + let mut editor = self.editor.lock().unwrap(); let layout_w = layout.bounds().width as i32; let layout_h = layout.bounds().height as i32; - buffer.set_size(layout_w, layout_h); - - if buffer.cursor_moved { - buffer.shape_until_cursor(); - buffer.cursor_moved = false; - } else { - buffer.shape_until_scroll(); - } + editor.buffer.set_size(layout_w, layout_h); + editor.shape_as_needed(); //TODO: redraw on color change - if buffer.redraw || pixels_opt.is_none() { + if editor.buffer.redraw || pixels_opt.is_none() { // Redraw buffer to image let instant = Instant::now(); let mut pixels = vec![0; layout_w as usize * layout_h as usize * 4]; - buffer.draw(&mut state.cache.lock().unwrap(), text_color, |start_x, start_y, w, h, color| { + editor.draw(&mut state.cache.lock().unwrap(), text_color, |start_x, start_y, w, h, color| { let alpha = (color.0 >> 24) & 0xFF; if alpha == 0 { // Do not draw if alpha is zero @@ -212,7 +207,7 @@ where *pixels_opt = Some((layout_w as u32, layout_h as u32, pixels)); - buffer.redraw = false; + editor.buffer.redraw = false; let duration = instant.elapsed(); log::debug!("redraw: {:?}", duration); @@ -235,64 +230,64 @@ where _shell: &mut Shell<'_, Message>, ) -> Status { let state = tree.state.downcast_mut::(); - let mut buffer = self.buffer.lock().unwrap(); + 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 => { - buffer.action(TextAction::Left); + editor.action(Action::Left); status = Status::Captured; }, KeyCode::Right => { - buffer.action(TextAction::Right); + editor.action(Action::Right); status = Status::Captured; }, KeyCode::Up => { - buffer.action(TextAction::Up); + editor.action(Action::Up); status = Status::Captured; }, KeyCode::Down => { - buffer.action(TextAction::Down); + editor.action(Action::Down); status = Status::Captured; }, KeyCode::Home => { - buffer.action(TextAction::Home); + editor.action(Action::Home); status = Status::Captured; }, KeyCode::End => { - buffer.action(TextAction::End); + editor.action(Action::End); status = Status::Captured; }, KeyCode::PageUp => { - buffer.action(TextAction::PageUp); + editor.action(Action::PageUp); status = Status::Captured; }, KeyCode::PageDown => { - buffer.action(TextAction::PageDown); + editor.action(Action::PageDown); status = Status::Captured; }, KeyCode::Enter => { - buffer.action(TextAction::Enter); + editor.action(Action::Enter); status = Status::Captured; }, KeyCode::Backspace => { - buffer.action(TextAction::Backspace); + editor.action(Action::Backspace); status = Status::Captured; }, KeyCode::Delete => { - buffer.action(TextAction::Delete); + editor.action(Action::Delete); status = Status::Captured; }, _ => () }, Event::Keyboard(KeyEvent::CharacterReceived(character)) => { - buffer.action(TextAction::Insert(character)); + editor.action(Action::Insert(character)); status = Status::Captured; }, Event::Mouse(MouseEvent::ButtonPressed(Button::Left)) => { if layout.bounds().contains(cursor_position) { - buffer.action(TextAction::Click { + editor.action(Action::Click { x: (cursor_position.x - layout.bounds().x) as i32, y: (cursor_position.y - layout.bounds().y) as i32, }); @@ -306,7 +301,7 @@ where }, Event::Mouse(MouseEvent::CursorMoved { .. }) => { if state.is_dragging { - buffer.action(TextAction::Drag { + editor.action(Action::Drag { x: (cursor_position.x - layout.bounds().x) as i32, y: (cursor_position.y - layout.bounds().y) as i32, }); @@ -315,7 +310,7 @@ where }, Event::Mouse(MouseEvent::WheelScrolled { delta }) => match delta { ScrollDelta::Lines { x, y } => { - buffer.action(TextAction::Scroll { + editor.action(Action::Scroll { lines: (-y * 6.0) as i32, }); status = Status::Captured; diff --git a/examples/editor-libcosmic/src/text_new.rs b/examples/editor-libcosmic/src/text_new.rs index 6c780fd..5703008 100644 --- a/examples/editor-libcosmic/src/text_new.rs +++ b/examples/editor-libcosmic/src/text_new.rs @@ -10,8 +10,8 @@ use cosmic_text::{ Attrs, AttrsList, SwashCache, - TextBufferLine, - TextMetrics, + BufferLine, + Metrics, }; use std::{ cmp, @@ -44,8 +44,8 @@ impl StyleSheet for Theme { } pub struct Text { - line: TextBufferLine<'static>, - metrics: TextMetrics, + line: BufferLine<'static>, + metrics: Metrics, } impl Text { @@ -53,7 +53,7 @@ impl Text { let instant = Instant::now(); //TODO: make it possible to set attrs - let mut line = TextBufferLine::new( + let mut line = BufferLine::new( string, AttrsList::new(Attrs::new()) ); @@ -63,7 +63,7 @@ impl Text { let text = Self { line, - metrics: TextMetrics::new(14, 20), + metrics: Metrics::new(14, 20), }; log::debug!("Text::new in {:?}", instant.elapsed()); diff --git a/examples/editor-orbclient/src/main.rs b/examples/editor-orbclient/src/main.rs index 4fd0a03..233f5d0 100644 --- a/examples/editor-orbclient/src/main.rs +++ b/examples/editor-orbclient/src/main.rs @@ -3,14 +3,15 @@ use cosmic_text::{ Attrs, AttrsList, + Buffer, Color, + Editor, Family, FontSystem, + Metrics, Style, SwashCache, - TextAction, - TextBuffer, - TextMetrics, + Action, Weight }; use orbclient::{EventOption, Renderer, Window, WindowFlag}; @@ -67,23 +68,23 @@ fn main() { let font_system = FontSystem::new(); let font_sizes = [ - TextMetrics::new(10, 14).scale(display_scale), // Caption - TextMetrics::new(14, 20).scale(display_scale), // Body - TextMetrics::new(20, 28).scale(display_scale), // Title 4 - TextMetrics::new(24, 32).scale(display_scale), // Title 3 - TextMetrics::new(28, 36).scale(display_scale), // Title 2 - TextMetrics::new(32, 44).scale(display_scale), // Title 1 + Metrics::new(10, 14).scale(display_scale), // Caption + Metrics::new(14, 20).scale(display_scale), // Body + Metrics::new(20, 28).scale(display_scale), // Title 4 + Metrics::new(24, 32).scale(display_scale), // Title 3 + Metrics::new(28, 36).scale(display_scale), // Title 2 + Metrics::new(32, 44).scale(display_scale), // Title 1 ]; let font_size_default = 1; // Body let mut font_size_i = font_size_default; let line_x = 8 * display_scale; - let mut buffer = TextBuffer::new( + let mut editor = Editor::new(Buffer::new( &font_system, font_sizes[font_size_i] - ); + )); - buffer.set_size( + editor.buffer.set_size( window.width() as i32 - line_x * 2, window.height() as i32 ); @@ -91,7 +92,7 @@ fn main() { let attrs = Attrs::new() .monospaced(true) .family(Family::Monospace); - buffer.set_text(&text, attrs); + editor.buffer.set_text(&text, attrs); let mut bg_color = orbclient::Color::rgb(0x00, 0x00, 0x00); let mut font_color = Color::rgb(0xFF, 0xFF, 0xFF); @@ -149,8 +150,8 @@ fn main() { if rehighlight { let now = Instant::now(); - for line_i in 0..buffer.lines.len() { - let line = &mut buffer.lines[line_i]; + for line_i in 0..editor.buffer.lines.len() { + let line = &mut editor.buffer.lines[line_i]; if ! line.is_reset() && line_i < syntax_cache.len() { continue; } @@ -209,8 +210,8 @@ fn main() { if line_i < syntax_cache.len() { if syntax_cache[line_i] != cache_item { syntax_cache[line_i] = cache_item; - if line_i + 1 < buffer.lines.len() { - buffer.lines[line_i + 1].reset(); + if line_i + 1 < editor.buffer.lines.len() { + editor.buffer.lines[line_i + 1].reset(); } } } else { @@ -218,25 +219,19 @@ fn main() { } } - buffer.redraw = true; + editor.buffer.redraw = true; rehighlight = false; log::info!("Syntax highlighted in {:?}", now.elapsed()); } - if buffer.cursor_moved { - buffer.shape_until_cursor(); - buffer.cursor_moved = false; - } else { - buffer.shape_until_scroll(); - } - - if buffer.redraw { + editor.shape_as_needed(); + if editor.buffer.redraw { let instant = Instant::now(); window.set(bg_color); - buffer.draw(&mut swash_cache, font_color, |x, y, w, h, color| { + editor.draw(&mut swash_cache, font_color, |x, y, w, h, color| { window.rect(line_x + x, y, w, h, orbclient::Color { data: color.0 }) }); @@ -244,7 +239,7 @@ fn main() { { let mut start_line_opt = None; let mut end_line = 0; - for run in buffer.layout_runs() { + for run in editor.buffer.layout_runs() { end_line = run.line_i; if start_line_opt == None { start_line_opt = Some(end_line); @@ -252,7 +247,7 @@ fn main() { } let start_line = start_line_opt.unwrap_or(end_line); - let lines = buffer.lines.len(); + let lines = editor.buffer.lines.len(); let start_y = (start_line * window.height() as usize) / lines; let end_y = (end_line * window.height() as usize) / lines; if end_y > start_y { @@ -268,7 +263,7 @@ fn main() { window.sync(); - buffer.redraw = false; + editor.buffer.redraw = false; let duration = instant.elapsed(); log::debug!("redraw: {:?}", duration); @@ -282,62 +277,62 @@ fn main() { match event.to_option() { EventOption::Key(event) => match event.scancode { orbclient::K_CTRL => ctrl_pressed = event.pressed, - orbclient::K_LEFT if event.pressed => buffer.action(TextAction::Left), - orbclient::K_RIGHT if event.pressed => buffer.action(TextAction::Right), - orbclient::K_UP if event.pressed => buffer.action(TextAction::Up), - orbclient::K_DOWN if event.pressed => buffer.action(TextAction::Down), - orbclient::K_HOME if event.pressed => buffer.action(TextAction::Home), - orbclient::K_END if event.pressed => buffer.action(TextAction::End), - orbclient::K_PGUP if event.pressed => buffer.action(TextAction::PageUp), - orbclient::K_PGDN if event.pressed => buffer.action(TextAction::PageDown), + orbclient::K_LEFT if event.pressed => editor.action(Action::Left), + orbclient::K_RIGHT if event.pressed => editor.action(Action::Right), + orbclient::K_UP if event.pressed => editor.action(Action::Up), + orbclient::K_DOWN if event.pressed => editor.action(Action::Down), + orbclient::K_HOME if event.pressed => editor.action(Action::Home), + 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_ENTER if event.pressed => { - buffer.action(TextAction::Enter); + editor.action(Action::Enter); rehighlight = true; }, orbclient::K_BKSP if event.pressed => { - buffer.action(TextAction::Backspace); + editor.action(Action::Backspace); rehighlight = true; }, orbclient::K_DEL if event.pressed => { - buffer.action(TextAction::Delete); + editor.action(Action::Delete); rehighlight = true; }, orbclient::K_0 if event.pressed && ctrl_pressed => { font_size_i = font_size_default; - buffer.set_metrics(font_sizes[font_size_i]); + editor.buffer.set_metrics(font_sizes[font_size_i]); } orbclient::K_MINUS if event.pressed && ctrl_pressed => { if font_size_i > 0 { font_size_i -= 1; - buffer.set_metrics(font_sizes[font_size_i]); + editor.buffer.set_metrics(font_sizes[font_size_i]); } } orbclient::K_EQUALS if event.pressed && ctrl_pressed => { if font_size_i + 1 < font_sizes.len() { font_size_i += 1; - buffer.set_metrics(font_sizes[font_size_i]); + editor.buffer.set_metrics(font_sizes[font_size_i]); } } _ => (), }, EventOption::TextInput(event) if !ctrl_pressed => { - buffer.action(TextAction::Insert(event.character)); + editor.action(Action::Insert(event.character)); rehighlight = true; } EventOption::Mouse(event) => { mouse_x = event.x; mouse_y = event.y; if mouse_left { - buffer.action(TextAction::Drag { + editor.action(Action::Drag { x: mouse_x - line_x, y: mouse_y, }); if mouse_y <= 5 { - buffer.action(TextAction::Scroll { lines: -3 }); + editor.action(Action::Scroll { lines: -3 }); window_async = true; } else if mouse_y + 5 >= window.height() as i32 { - buffer.action(TextAction::Scroll { lines: 3 }); + editor.action(Action::Scroll { lines: 3 }); window_async = true; } @@ -348,7 +343,7 @@ fn main() { if event.left != mouse_left { mouse_left = event.left; if mouse_left { - buffer.action(TextAction::Click { + editor.action(Action::Click { x: mouse_x - line_x, y: mouse_y, }); @@ -357,11 +352,10 @@ fn main() { } } EventOption::Resize(event) => { - buffer.set_size(event.width as i32 - line_x * 2, event.height as i32); - buffer.redraw = true; + editor.buffer.set_size(event.width as i32 - line_x * 2, event.height as i32); } EventOption::Scroll(event) => { - buffer.action(TextAction::Scroll { + editor.action(Action::Scroll { lines: -event.y * 3, }); } @@ -371,16 +365,16 @@ fn main() { } if mouse_left && force_drag { - buffer.action(TextAction::Drag { + editor.action(Action::Drag { x: mouse_x - line_x, y: mouse_y, }); if mouse_y <= 5 { - buffer.action(TextAction::Scroll { lines: -3 }); + editor.action(Action::Scroll { lines: -3 }); window_async = true; } else if mouse_y + 5 >= window.height() as i32 { - buffer.action(TextAction::Scroll { lines: 3 }); + editor.action(Action::Scroll { lines: 3 }); window_async = true; } } diff --git a/examples/editor-test/src/main.rs b/examples/editor-test/src/main.rs index 413a6eb..eaf299e 100644 --- a/examples/editor-test/src/main.rs +++ b/examples/editor-test/src/main.rs @@ -1,33 +1,27 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -use cosmic_text::{Color, FontSystem, SwashCache, TextAction, TextBuffer, TextMetrics}; +use cosmic_text::{Action, Buffer, Color, Editor, FontSystem, Metrics, SwashCache}; use orbclient::{EventOption, Renderer, Window, WindowFlag}; use std::{env, fs, process, thread, time::{Duration, Instant}}; use unicode_segmentation::UnicodeSegmentation; -fn redraw(window: &mut Window, buffer: &mut TextBuffer<'_>, swash_cache: &mut SwashCache) { +fn redraw(window: &mut Window, editor: &mut Editor<'_>, swash_cache: &mut SwashCache) { let bg_color = orbclient::Color::rgb(0x34, 0x34, 0x34); let font_color = Color::rgb(0xFF, 0xFF, 0xFF); - if buffer.cursor_moved { - buffer.shape_until_cursor(); - buffer.cursor_moved = false; - } else { - buffer.shape_until_scroll(); - } - - if buffer.redraw { + editor.shape_as_needed(); + if editor.buffer.redraw { let instant = Instant::now(); window.set(bg_color); - buffer.draw(swash_cache, font_color, |x, y, w, h, color| { + editor.draw(swash_cache, font_color, |x, y, w, h, color| { window.rect(x, y, w, h, orbclient::Color { data: color.0 }); }); window.sync(); - buffer.redraw = false; + editor.buffer.redraw = false; let duration = instant.elapsed(); log::debug!("redraw: {:?}", duration); @@ -51,16 +45,16 @@ fn main() { .unwrap(); let font_sizes = [ - TextMetrics::new(10, 14).scale(display_scale), // Caption - TextMetrics::new(14, 20).scale(display_scale), // Body - TextMetrics::new(20, 28).scale(display_scale), // Title 4 - TextMetrics::new(24, 32).scale(display_scale), // Title 3 - TextMetrics::new(28, 36).scale(display_scale), // Title 2 - TextMetrics::new(32, 44).scale(display_scale), // Title 1 + Metrics::new(10, 14).scale(display_scale), // Caption + Metrics::new(14, 20).scale(display_scale), // Body + Metrics::new(20, 28).scale(display_scale), // Title 4 + Metrics::new(24, 32).scale(display_scale), // Title 3 + Metrics::new(28, 36).scale(display_scale), // Title 2 + Metrics::new(32, 44).scale(display_scale), // Title 1 ]; let font_size_default = 1; // Body - let mut buffer = TextBuffer::new( + let mut buffer = Buffer::new( &font_system, font_sizes[font_size_default] ); @@ -69,6 +63,8 @@ fn main() { window.height() as i32 ); + let mut editor = Editor::new(buffer); + let mut swash_cache = SwashCache::new(&font_system); let text = if let Some(arg) = env::args().nth(1) { @@ -93,49 +89,49 @@ fn main() { // Test backspace of character { - let cursor = buffer.cursor(); - buffer.action(TextAction::Insert(c)); - buffer.action(TextAction::Backspace); - assert_eq!(cursor, buffer.cursor()); + let cursor = editor.cursor(); + editor.action(Action::Insert(c)); + editor.action(Action::Backspace); + assert_eq!(cursor, editor.cursor()); } // Finally, normal insert of character - buffer.action(TextAction::Insert(c)); + editor.action(Action::Insert(c)); } // Test delete of EGC { - let cursor = buffer.cursor(); - buffer.action(TextAction::Previous); - buffer.action(TextAction::Delete); + let cursor = editor.cursor(); + editor.action(Action::Previous); + editor.action(Action::Delete); for c in grapheme.chars() { - buffer.action(TextAction::Insert(c)); + editor.action(Action::Insert(c)); } - assert_eq!(cursor, buffer.cursor()); + assert_eq!(cursor, editor.cursor()); } } // Test backspace of newline { - let cursor = buffer.cursor(); - buffer.action(TextAction::Enter); - buffer.action(TextAction::Backspace); - assert_eq!(cursor, buffer.cursor()); + let cursor = editor.cursor(); + editor.action(Action::Enter); + editor.action(Action::Backspace); + assert_eq!(cursor, editor.cursor()); } // Test delete of newline { - let cursor = buffer.cursor(); - buffer.action(TextAction::Enter); - buffer.action(TextAction::Previous); - buffer.action(TextAction::Delete); - assert_eq!(cursor, buffer.cursor()); + let cursor = editor.cursor(); + editor.action(Action::Enter); + editor.action(Action::Previous); + editor.action(Action::Delete); + assert_eq!(cursor, editor.cursor()); } // Finally, normal enter - buffer.action(TextAction::Enter); + editor.action(Action::Enter); - redraw(&mut window, &mut buffer, &mut swash_cache); + redraw(&mut window, &mut editor, &mut swash_cache); for event in window.events() { match event.to_option() { @@ -150,7 +146,7 @@ fn main() { let mut wrong = 0; for (line_i, line) in text.lines().enumerate() { - let buffer_line = &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; diff --git a/examples/rich-text/src/main.rs b/examples/rich-text/src/main.rs index 55859a8..a54400d 100644 --- a/examples/rich-text/src/main.rs +++ b/examples/rich-text/src/main.rs @@ -1,7 +1,20 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -use cosmic_text::{Attrs, AttrsList, Color, Family, FontSystem, Style, SwashCache, - TextAction, TextBuffer, TextBufferLine, TextMetrics, Weight}; +use cosmic_text::{ + Action, + Attrs, + AttrsList, + Buffer, + BufferLine, + Color, + Editor, + Family, + FontSystem, + Metrics, + Style, + SwashCache, + Weight, +}; use orbclient::{EventOption, Renderer, Window, WindowFlag}; use std::{process, thread, time::{Duration, Instant}}; @@ -31,12 +44,12 @@ fn main() { ) .unwrap(); - let mut buffer = TextBuffer::new( + let mut editor = Editor::new(Buffer::new( &font_system, - TextMetrics::new(32, 44).scale(display_scale) - ); + Metrics::new(32, 44).scale(display_scale) + )); - buffer.set_size( + editor.buffer.set_size( window.width() as i32, window.height() as i32 ); @@ -46,7 +59,7 @@ fn main() { let mono_attrs = attrs.monospaced(true).family(Family::Monospace); let comic_attrs = attrs.family(Family::Name("Comic Neue")); - buffer.lines.clear(); + editor.buffer.lines.clear(); let lines: &[&[(&str, Attrs)]] = &[ &[ @@ -118,7 +131,7 @@ fn main() { let end = line_text.len(); attrs_list.add_span(start..end, attrs); } - buffer.lines.push(TextBufferLine::new(line_text, attrs_list)); + editor.buffer.lines.push(BufferLine::new(line_text, attrs_list)); } let mut swash_cache = SwashCache::new(&font_system); @@ -131,25 +144,19 @@ fn main() { let bg_color = orbclient::Color::rgb(0x34, 0x34, 0x34); let font_color = Color::rgb(0xFF, 0xFF, 0xFF); - if buffer.cursor_moved { - buffer.shape_until_cursor(); - buffer.cursor_moved = false; - } else { - buffer.shape_until_scroll(); - } - - if buffer.redraw { + editor.shape_as_needed(); + if editor.buffer.redraw { let instant = Instant::now(); window.set(bg_color); - buffer.draw(&mut swash_cache, font_color, |x, y, w, h, color| { + editor.draw(&mut swash_cache, font_color, |x, y, w, h, color| { window.rect(x, y, w, h, orbclient::Color { data: color.0 }); }); window.sync(); - buffer.redraw = false; + editor.buffer.redraw = false; let duration = instant.elapsed(); log::debug!("redraw: {:?}", duration); @@ -158,36 +165,35 @@ fn main() { for event in window.events() { match event.to_option() { EventOption::Key(event) => match event.scancode { - orbclient::K_LEFT if event.pressed => buffer.action(TextAction::Left), - orbclient::K_RIGHT if event.pressed => buffer.action(TextAction::Right), - orbclient::K_UP if event.pressed => buffer.action(TextAction::Up), - orbclient::K_DOWN if event.pressed => buffer.action(TextAction::Down), - orbclient::K_HOME if event.pressed => buffer.action(TextAction::Home), - orbclient::K_END if event.pressed => buffer.action(TextAction::End), - orbclient::K_PGUP if event.pressed => buffer.action(TextAction::PageUp), - orbclient::K_PGDN if event.pressed => buffer.action(TextAction::PageDown), - orbclient::K_ENTER if event.pressed => buffer.action(TextAction::Enter), - orbclient::K_BKSP if event.pressed => buffer.action(TextAction::Backspace), - orbclient::K_DEL if event.pressed => buffer.action(TextAction::Delete), + orbclient::K_LEFT if event.pressed => editor.action(Action::Left), + orbclient::K_RIGHT if event.pressed => editor.action(Action::Right), + orbclient::K_UP if event.pressed => editor.action(Action::Up), + orbclient::K_DOWN if event.pressed => editor.action(Action::Down), + orbclient::K_HOME if event.pressed => editor.action(Action::Home), + 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_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), _ => (), }, - EventOption::TextInput(event) => buffer.action(TextAction::Insert(event.character)), + EventOption::TextInput(event) => editor.action(Action::Insert(event.character)), EventOption::Mouse(mouse) => { mouse_x = mouse.x; mouse_y = mouse.y; if mouse_left { - buffer.action(TextAction::Drag { x: mouse_x, y: mouse_y }); + editor.action(Action::Drag { x: mouse_x, y: mouse_y }); } }, EventOption::Button(button) => { mouse_left = button.left; if mouse_left { - buffer.action(TextAction::Click { x: mouse_x, y: mouse_y }); + editor.action(Action::Click { x: mouse_x, y: mouse_y }); } }, EventOption::Resize(resize) => { - buffer.set_size(resize.width as i32, resize.height as i32); - buffer.redraw = true; + editor.buffer.set_size(resize.width as i32, resize.height as i32); }, EventOption::Quit(_) => process::exit(0), _ => (), diff --git a/examples/terminal/src/main.rs b/examples/terminal/src/main.rs index c6f5dbc..2a06e6b 100644 --- a/examples/terminal/src/main.rs +++ b/examples/terminal/src/main.rs @@ -1,4 +1,4 @@ -use cosmic_text::{Attrs, Color, FontSystem, SwashCache, TextBuffer, TextMetrics}; +use cosmic_text::{Attrs, Color, FontSystem, SwashCache, Buffer, Metrics}; use std::cmp; use termion::{ color, @@ -13,24 +13,24 @@ fn main() { let mut swash_cache = SwashCache::new(&font_system); // Text metrics indicate the font size and line height of a buffer - let metrics = TextMetrics::new(14, 20); + let metrics = Metrics::new(14, 20); - // A TextBuffer provides shaping and layout for a UTF-8 string, create one per text widget - let mut text_buffer = TextBuffer::new(&font_system, metrics); + // A Buffer provides shaping and layout for a UTF-8 string, create one per text widget + let mut buffer = Buffer::new(&font_system, metrics); // Set a size for the text buffer, in pixels let width = 80u16; let height = 25u16; - text_buffer.set_size(width as i32, height as i32); + buffer.set_size(width as i32, height as i32); // Attributes indicate what font to choose let attrs = Attrs::new(); // Add some text! - text_buffer.set_text(" Hi, Rust! 🦀", attrs); + buffer.set_text(" Hi, Rust! 🦀", attrs); // Perform shaping as desired - text_buffer.shape_until_cursor(); + buffer.shape_until_cursor(); // Default text color (0xFF, 0xFF, 0xFF is white) let text_color = Color::rgb(0xFF, 0xFF, 0xFF); @@ -40,7 +40,7 @@ fn main() { // Clear buffer with black background for _y in 0..height { - for _x in 0..text_buffer.size().0 { + for _x in 0..buffer.size().0 { print!( "{} {}", color::Bg(color::Rgb(0, 0, 0)), @@ -56,7 +56,7 @@ fn main() { // Print the buffer let mut last_x = 0; let mut last_y = 0; - text_buffer.draw(&mut swash_cache, text_color, |x, y, w, h, color| { + buffer.draw(&mut swash_cache, text_color, |x, y, w, h, color| { let a = color.a(); if a == 0 || x < 0 || y < 0 || w != 1 || h != 1 { // Ignore alphas of 0, or invalid x, y coordinates, or unimplemented sizes diff --git a/src/buffer.rs b/src/buffer.rs index a61dd6c..fef7efb 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -7,77 +7,38 @@ use std::{ }; use unicode_segmentation::UnicodeSegmentation; -use crate::{Attrs, AttrsList, Color, FontSystem, LayoutGlyph, TextBufferLine}; - -/// An action to perform on a [TextBuffer] -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum TextAction { - /// 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 }, -} +use crate::{Attrs, AttrsList, BufferLine, Color, FontSystem, LayoutGlyph, LayoutLine, ShapeLine}; /// Current cursor location #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] -pub struct TextCursor { +pub struct Cursor { /// Text line the cursor is on pub line: usize, /// Index of glyph at cursor (will insert behind this glyph) pub index: usize, } -impl TextCursor { +impl Cursor { /// Create a new cursor pub const fn new(line: usize, index: usize) -> Self { Self { line, index } } } -struct TextLayoutCursor { - line: usize, - layout: usize, - glyph: usize, +pub struct LayoutCursor { + pub line: usize, + pub layout: usize, + pub glyph: usize, } -impl TextLayoutCursor { - fn new(line: usize, layout: usize, glyph: usize) -> Self { +impl LayoutCursor { + pub fn new(line: usize, layout: usize, glyph: usize) -> Self { Self { line, layout, glyph } } } /// A line of visible text for rendering -pub struct TextLayoutRun<'a> { +pub struct LayoutRun<'a> { /// The index of the original text line pub line_i: usize, /// The original text line @@ -90,17 +51,17 @@ pub struct TextLayoutRun<'a> { pub line_y: i32, } -/// An iterator of visible text lines, see [TextLayoutRun] -pub struct TextLayoutRunIter<'a, 'b> { - buffer: &'b TextBuffer<'a>, +/// An iterator of visible text lines, see [LayoutRun] +pub struct LayoutRunIter<'a, 'b> { + buffer: &'b Buffer<'a>, line_i: usize, layout_i: usize, line_y: i32, total_layout: i32, } -impl<'a, 'b> TextLayoutRunIter<'a, 'b> { - pub fn new(buffer: &'b TextBuffer<'a>) -> Self { +impl<'a, 'b> LayoutRunIter<'a, 'b> { + pub fn new(buffer: &'b Buffer<'a>) -> Self { Self { buffer, line_i: 0, @@ -111,8 +72,8 @@ impl<'a, 'b> TextLayoutRunIter<'a, 'b> { } } -impl<'a, 'b> Iterator for TextLayoutRunIter<'a, 'b> { - type Item = TextLayoutRun<'b>; +impl<'a, 'b> Iterator for LayoutRunIter<'a, 'b> { + type Item = LayoutRun<'b>; fn next(&mut self) -> Option { while let Some(line) = self.buffer.lines.get(self.line_i) { @@ -132,7 +93,7 @@ impl<'a, 'b> Iterator for TextLayoutRunIter<'a, 'b> { return None; } - return Some(TextLayoutRun { + return Some(LayoutRun { line_i: self.line_i, text: line.text(), rtl: shape.rtl, @@ -150,14 +111,14 @@ impl<'a, 'b> Iterator for TextLayoutRunIter<'a, 'b> { /// Metrics of text #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] -pub struct TextMetrics { +pub struct Metrics { /// Font size in pixels pub font_size: i32, /// Line height in pixels pub line_height: i32, } -impl TextMetrics { +impl Metrics { pub const fn new(font_size: i32, line_height: i32) -> Self { Self { font_size, line_height } } @@ -170,37 +131,29 @@ impl TextMetrics { } } -impl fmt::Display for TextMetrics { +impl fmt::Display for Metrics { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}px / {}px", self.font_size, self.line_height) } } /// A buffer of text that is shaped and laid out -pub struct TextBuffer<'a> { +pub struct Buffer<'a> { font_system: &'a FontSystem<'a>, /// Lines (or paragraphs) of text in the buffer - pub lines: Vec>, - metrics: TextMetrics, + pub lines: Vec>, + metrics: Metrics, width: i32, height: i32, scroll: i32, - cursor: TextCursor, - cursor_x_opt: Option, - select_opt: Option, - /// True if the cursor has been moved. Set to false after processing - /// - /// Usually, if this is true, you should run [Self::shape_until_cursor] before redrawing. - /// Otherwise, you should run [Self::shape_until_scroll] - pub cursor_moved: bool, /// True if a redraw is requires. Set to false after processing pub redraw: bool, } -impl<'a> TextBuffer<'a> { +impl<'a> Buffer<'a> { pub fn new( font_system: &'a FontSystem<'a>, - metrics: TextMetrics, + metrics: Metrics, ) -> Self { let mut buffer = Self { font_system, @@ -209,16 +162,32 @@ impl<'a> TextBuffer<'a> { width: 0, height: 0, scroll: 0, - cursor: TextCursor::default(), - cursor_x_opt: None, - select_opt: None, - cursor_moved: false, redraw: false, }; buffer.set_text("", Attrs::new()); buffer } + fn relayout(&mut self) { + let instant = Instant::now(); + + for line in self.lines.iter_mut() { + if line.shape_opt().is_some() { + line.reset_layout(); + line.layout( + self.font_system, + self.metrics.font_size, + self.width + ); + } + } + + self.redraw = true; + + let duration = instant.elapsed(); + log::debug!("relayout: {:?}", duration); + } + /// Pre-shape lines in the buffer, up to `lines`, return actual number of layout lines pub fn shape_until(&mut self, lines: i32) -> i32 { let instant = Instant::now(); @@ -251,13 +220,13 @@ impl<'a> TextBuffer<'a> { } /// Shape lines until cursor, also scrolling to include cursor in view - pub fn shape_until_cursor(&mut self) { + pub fn shape_until_cursor(&mut self, cursor: Cursor) { let instant = Instant::now(); let mut reshaped = 0; let mut layout_i = 0; for (line_i, line) in self.lines.iter_mut().enumerate() { - if line_i > self.cursor.line { + if line_i > cursor.line { break; } @@ -269,8 +238,8 @@ impl<'a> TextBuffer<'a> { self.metrics.font_size, self.width ); - if line_i == self.cursor.line { - let layout_cursor = self.layout_cursor(&self.cursor); + if line_i == cursor.line { + let layout_cursor = self.layout_cursor(&cursor); layout_i += layout_cursor.layout as i32; break; } else { @@ -284,7 +253,7 @@ impl<'a> TextBuffer<'a> { self.redraw = true; } - let lines = self.lines(); + let lines = self.visible_lines(); if layout_i < self.scroll { self.scroll = layout_i; } else if layout_i >= self.scroll + lines { @@ -296,7 +265,7 @@ impl<'a> TextBuffer<'a> { /// Shape lines until scroll pub fn shape_until_scroll(&mut self) { - let lines = self.lines(); + let lines = self.visible_lines(); let scroll_end = self.scroll + lines; let total_layout = self.shape_until(scroll_end); @@ -310,34 +279,14 @@ impl<'a> TextBuffer<'a> { ); } - fn relayout(&mut self) { - let instant = Instant::now(); - - for line in self.lines.iter_mut() { - if line.shape_opt().is_some() { - line.reset_layout(); - line.layout( - self.font_system, - self.metrics.font_size, - self.width - ); - } - } - - self.redraw = true; - - let duration = instant.elapsed(); - log::debug!("relayout: {:?}", duration); - } - - fn layout_cursor(&self, cursor: &TextCursor) -> TextLayoutCursor { + 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? for (layout_i, layout_line) in layout.iter().enumerate() { for (glyph_i, glyph) in layout_line.glyphs.iter().enumerate() { if cursor.index == glyph.start { - return TextLayoutCursor::new( + return LayoutCursor::new( cursor.line, layout_i, glyph_i @@ -347,7 +296,7 @@ impl<'a> TextBuffer<'a> { match layout_line.glyphs.last() { Some(glyph) => { if cursor.index == glyph.end { - return TextLayoutCursor::new( + return LayoutCursor::new( cursor.line, layout_i, layout_line.glyphs.len() @@ -355,7 +304,7 @@ impl<'a> TextBuffer<'a> { } }, None => { - return TextLayoutCursor::new( + return LayoutCursor::new( cursor.line, layout_i, 0 @@ -366,57 +315,30 @@ impl<'a> TextBuffer<'a> { // Fall back to start of line //TODO: should this be the end of the line? - TextLayoutCursor::new( + LayoutCursor::new( cursor.line, 0, 0 ) } - fn set_layout_cursor(&mut self, cursor: TextLayoutCursor) { - let line = &mut self.lines[cursor.line]; - let layout = line.layout( - self.font_system, - self.metrics.font_size, - self.width - ); - - let layout_line = match layout.get(cursor.layout) { - Some(some) => some, - None => match layout.last() { - Some(some) => some, - None => todo!("layout cursor in line with no layouts"), - } - }; - - let new_index = match layout_line.glyphs.get(cursor.glyph) { - Some(glyph) => glyph.start, - None => match layout_line.glyphs.last() { - Some(glyph) => glyph.end, - //TODO: is this correct? - None => 0, - } - }; - - if self.cursor.line != cursor.line || self.cursor.index != new_index { - self.cursor.line = cursor.line; - self.cursor.index = new_index; - self.redraw = true; - } + 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)) } - /// Get the current cursor position - pub fn cursor(&self) -> TextCursor { - self.cursor + 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)) } - /// Get the current [TextMetrics] - pub fn metrics(&self) -> TextMetrics { + /// Get the current [Metrics] + pub fn metrics(&self) -> Metrics { self.metrics } - /// Set the current [TextMetrics] - pub fn set_metrics(&mut self, metrics: TextMetrics) { + /// Set the current [Metrics] + pub fn set_metrics(&mut self, metrics: Metrics) { if metrics != self.metrics { self.metrics = metrics; self.relayout(); @@ -448,8 +370,15 @@ impl<'a> TextBuffer<'a> { self.scroll } + pub fn set_scroll(&mut self, scroll: i32) { + if scroll != self.scroll { + self.scroll = scroll; + self.redraw = true; + } + } + /// Get the number of lines that can be viewed in the buffer - pub fn lines(&self) -> i32 { + pub fn visible_lines(&self) -> i32 { self.height / self.metrics.line_height } @@ -457,325 +386,25 @@ impl<'a> TextBuffer<'a> { pub fn set_text(&mut self, text: &str, attrs: Attrs<'a>) { self.lines.clear(); for line in text.lines() { - self.lines.push(TextBufferLine::new(line.to_string(), AttrsList::new(attrs))); + self.lines.push(BufferLine::new(line.to_string(), AttrsList::new(attrs))); } // Make sure there is always one line if self.lines.is_empty() { - self.lines.push(TextBufferLine::new(String::new(), AttrsList::new(attrs))); + self.lines.push(BufferLine::new(String::new(), AttrsList::new(attrs))); } self.scroll = 0; - self.cursor = TextCursor::default(); - self.select_opt = None; self.shape_until_scroll(); } - /// Perform a [TextAction] on the buffer - pub fn action(&mut self, action: TextAction) { - let old_cursor = self.cursor; - - match action { - TextAction::Previous => { - let line = &mut self.lines[self.cursor.line]; - if self.cursor.index > 0 { - // Find previous character index - let mut prev_index = 0; - for (i, _) in line.text().grapheme_indices(true) { - if i < self.cursor.index { - prev_index = i; - } else { - break; - } - } - - self.cursor.index = prev_index; - self.redraw = true; - } else if self.cursor.line > 0 { - self.cursor.line -= 1; - self.cursor.index = self.lines[self.cursor.line].text().len(); - self.redraw = true; - } - self.cursor_x_opt = None; - }, - TextAction::Next => { - let line = &mut self.lines[self.cursor.line]; - if self.cursor.index < line.text().len() { - for (i, c) in line.text().grapheme_indices(true) { - if i == self.cursor.index { - self.cursor.index += c.len(); - self.redraw = true; - break; - } - } - } else if self.cursor.line + 1 < self.lines.len() { - self.cursor.line += 1; - self.cursor.index = 0; - self.redraw = true; - } - self.cursor_x_opt = None; - }, - TextAction::Left => { - let rtl_opt = self.lines[self.cursor.line].shape_opt().as_ref().map(|shape| shape.rtl); - if let Some(rtl) = rtl_opt { - if rtl { - self.action(TextAction::Next); - } else { - self.action(TextAction::Previous); - } - } - }, - TextAction::Right => { - let rtl_opt = self.lines[self.cursor.line].shape_opt().as_ref().map(|shape| shape.rtl); - if let Some(rtl) = rtl_opt { - if rtl { - self.action(TextAction::Previous); - } else { - self.action(TextAction::Next); - } - } - }, - TextAction::Up => { - //TODO: make this preserve X as best as possible! - let mut cursor = self.layout_cursor(&self.cursor); - - if self.cursor_x_opt.is_none() { - self.cursor_x_opt = Some( - cursor.glyph as i32 //TODO: glyph x position - ); - } - - if cursor.layout > 0 { - cursor.layout -= 1; - } else if cursor.line > 0 { - cursor.line -= 1; - cursor.layout = usize::max_value(); - } - - if let Some(cursor_x) = self.cursor_x_opt { - cursor.glyph = cursor_x as usize; //TODO: glyph x position - } - - self.set_layout_cursor(cursor); - }, - TextAction::Down => { - //TODO: make this preserve X as best as possible! - let mut cursor = self.layout_cursor(&self.cursor); - - let layout_len = { - let line = &mut self.lines[cursor.line]; - let layout = line.layout( - self.font_system, - self.metrics.font_size, - self.width - ); - layout.len() - }; - - if self.cursor_x_opt.is_none() { - self.cursor_x_opt = Some( - cursor.glyph as i32 //TODO: glyph x position - ); - } - - if cursor.layout + 1 < layout_len { - cursor.layout += 1; - } else if cursor.line + 1 < self.lines.len() { - cursor.line += 1; - cursor.layout = 0; - } - - if let Some(cursor_x) = self.cursor_x_opt { - cursor.glyph = cursor_x as usize; //TODO: glyph x position - } - - self.set_layout_cursor(cursor); - }, - TextAction::Home => { - let mut cursor = self.layout_cursor(&self.cursor); - cursor.glyph = 0; - self.set_layout_cursor(cursor); - self.cursor_x_opt = None; - }, - TextAction::End => { - let mut cursor = self.layout_cursor(&self.cursor); - cursor.glyph = usize::max_value(); - self.set_layout_cursor(cursor); - self.cursor_x_opt = None; - } - TextAction::PageUp => { - //TODO: move cursor - self.scroll -= self.lines(); - self.redraw = true; - - self.shape_until_scroll(); - }, - TextAction::PageDown => { - //TODO: move cursor - self.scroll += self.lines(); - self.redraw = true; - - self.shape_until_scroll(); - }, - TextAction::Insert(character) => { - if character.is_control() - && !['\t', '\u{92}'].contains(&character) - { - // Filter out special chars (except for tab), use TextAction instead - log::debug!("Refusing to insert control character {:?}", character); - } else { - let line = &mut self.lines[self.cursor.line]; - - println!("Before"); - for span in line.attrs_list().spans() { - println!("{:?}", span); - } - - // Collect text after insertion as a line - let after = line.split_off(self.cursor.index); - - // Append the inserted text - line.append(TextBufferLine::new( - character.to_string(), - AttrsList::new(line.attrs_list().defaults() /*TODO: provide attrs?*/) - )); - - // Append the text after insertion - line.append(after); - - println!("After"); - for span in line.attrs_list().spans() { - println!("{:?}", span); - } - - self.cursor.index += character.len_utf8(); - } - }, - TextAction::Enter => { - let new_line = self.lines[self.cursor.line].split_off(self.cursor.index); - - self.cursor.line += 1; - self.cursor.index = 0; - - self.lines.insert(self.cursor.line, new_line); - }, - TextAction::Backspace => { - if self.cursor.index > 0 { - let line = &mut self.lines[self.cursor.line]; - - println!("Before"); - for span in line.attrs_list().spans() { - println!("{:?}", span); - } - - // Get text line after cursor - let after = line.split_off(self.cursor.index); - - // Find previous character index - let mut prev_index = 0; - for (i, _) in line.text().char_indices() { - if i < self.cursor.index { - prev_index = i; - } else { - break; - } - } - - println!("Move cursor {} to {}", self.cursor.index, prev_index); - - self.cursor.index = prev_index; - - // Remove character - line.split_off(self.cursor.index); - - // Add text after cursor - line.append(after); - - println!("After"); - for span in line.attrs_list().spans() { - println!("{:?}", span); - } - } else if self.cursor.line > 0 { - let mut line_index = self.cursor.line; - let old_line = self.lines.remove(line_index); - line_index -= 1; - - let line = &mut self.lines[line_index]; - - self.cursor.line = line_index; - self.cursor.index = line.text().len(); - - line.append(old_line); - } - }, - TextAction::Delete => { - if self.cursor.index < self.lines[self.cursor.line].text().len() { - let line = &mut self.lines[self.cursor.line]; - - let range_opt = line - .text() - .grapheme_indices(true) - .take_while(|(i, _)| *i <= self.cursor.index) - .last() - .map(|(i, c)| { - i..(i + c.len()) - }); - - if let Some(range) = range_opt { - self.cursor.index = range.start; - - // Get text after deleted EGC - let after = line.split_off(range.end); - - // Delete EGC - line.split_off(range.start); - - // Add text after deleted EGC - line.append(after); - } - } else if self.cursor.line + 1 < self.lines.len() { - let old_line = self.lines.remove(self.cursor.line + 1); - self.lines[self.cursor.line].append(old_line); - } - }, - TextAction::Click { x, y } => { - self.select_opt = None; - - if let Some(new_cursor) = self.hit(x, y) { - if new_cursor != self.cursor { - self.cursor = new_cursor; - self.redraw = true; - } - } - }, - TextAction::Drag { x, y } => { - if self.select_opt.is_none() { - self.select_opt = Some(self.cursor); - self.redraw = true; - } - - if let Some(new_cursor) = self.hit(x, y) { - if new_cursor != self.cursor { - self.cursor = new_cursor; - self.redraw = true; - } - } - }, - TextAction::Scroll { lines } => { - self.scroll += lines; - self.redraw = true; - - self.shape_until_scroll(); - } - } - - if old_cursor != self.cursor { - self.cursor_moved = true; - } + /// Get the visible layout runs for rendering and other tasks + pub fn layout_runs<'b>(&'b self) -> LayoutRunIter<'a, 'b> { + LayoutRunIter::new(self) } - /// Convert x, y position to TextCursor (hit detection) - pub fn hit(&self, x: i32, y: i32) -> Option { + /// Convert x, y position to Cursor (hit detection) + pub fn hit(&self, x: i32, y: i32) -> Option { let instant = Instant::now(); let font_size = self.metrics.font_size; @@ -826,7 +455,7 @@ impl<'a> TextBuffer<'a> { } } - let mut new_cursor = TextCursor::new(run.line_i, 0); + let mut new_cursor = Cursor::new(run.line_i, 0); match run.glyphs.get(new_cursor_glyph) { Some(glyph) => { @@ -839,22 +468,6 @@ impl<'a> TextBuffer<'a> { }, } - if new_cursor != self.cursor { - if let Some(glyph) = run.glyphs.get(new_cursor_glyph) { - let font_opt = self.font_system.get_font(glyph.cache_key.font_id); - let text_glyph = &run.text[glyph.start..glyph.end]; - log::debug!( - "{}, {}: '{}' ('{}'): '{}' ({:?})", - self.cursor.line, - self.cursor.index, - font_opt.as_ref().map_or("?", |font| font.info.family.as_str()), - font_opt.as_ref().map_or("?", |font| font.info.post_script_name.as_str()), - text_glyph, - text_glyph - ); - } - } - new_cursor_opt = Some(new_cursor); break; @@ -867,172 +480,12 @@ impl<'a> TextBuffer<'a> { new_cursor_opt } - /// Get the visible layout runs for rendering and other tasks - pub fn layout_runs<'b>(&'b self) -> TextLayoutRunIter<'a, 'b> { - TextLayoutRunIter::new(self) - } - /// Draw the buffer #[cfg(feature = "swash")] pub fn draw(&self, cache: &mut crate::SwashCache, color: Color, mut f: F) where F: FnMut(i32, i32, u32, u32, Color) { - let font_size = self.metrics.font_size; - let line_height = self.metrics.line_height; - for run in self.layout_runs() { - let line_i = run.line_i; - let line_y = run.line_y; - - let cursor_glyph_opt = |cursor: &TextCursor| -> Option<(usize, f32)> { - if cursor.line == line_i { - for (glyph_i, glyph) in run.glyphs.iter().enumerate() { - if cursor.index == glyph.start { - return Some((glyph_i, 0.0)); - } else 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 offset = glyph.w * (before as f32) / (total as f32); - return Some((glyph_i, offset)); - } - } - match run.glyphs.last() { - Some(glyph) => { - if cursor.index == glyph.end { - return Some((run.glyphs.len(), 0.0)); - } - }, - None => { - return Some((0, 0.0)); - } - } - } - 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.width)); - } - - 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.width; - } - } - 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_opt(&self.cursor) { - let 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 - } else { - (glyph.x + cursor_glyph_offset) as i32 - } - }, - None => match run.glyphs.last() { - Some(glyph) => { - // End of last glyph - if glyph.rtl { - glyph.x as i32 - } else { - (glyph.x + glyph.w) as i32 - } - }, - None => { - // Start of empty line - 0 - } - } - }; - - f( - 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); @@ -1042,7 +495,7 @@ impl<'a> TextBuffer<'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, run.line_y + y_int + y, 1, 1, color) }); } } diff --git a/src/buffer_line.rs b/src/buffer_line.rs index 304ba99..c9fecba 100644 --- a/src/buffer_line.rs +++ b/src/buffer_line.rs @@ -1,7 +1,7 @@ use crate::{AttrsList, FontSystem, LayoutLine, ShapeLine}; /// A line (or paragraph) of text that is shaped and laid out -pub struct TextBufferLine<'a> { +pub struct BufferLine<'a> { //TODO: make this not pub(crate) text: String, attrs_list: AttrsList<'a>, @@ -10,7 +10,7 @@ pub struct TextBufferLine<'a> { layout_opt: Option>, } -impl<'a> TextBufferLine<'a> { +impl<'a> BufferLine<'a> { /// 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 diff --git a/src/editor.rs b/src/editor.rs new file mode 100644 index 0000000..052ca6c --- /dev/null +++ b/src/editor.rs @@ -0,0 +1,595 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use std::cmp; +use unicode_segmentation::UnicodeSegmentation; + +use crate::{AttrsList, Buffer, BufferLine, Color, Cursor, LayoutCursor}; + +/// 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 +pub struct Editor<'a> { + pub buffer: Buffer<'a>, + cursor: Cursor, + cursor_x_opt: Option, + select_opt: Option, + /// True if the cursor has been moved. Set to false after processing + /// + /// Usually, if this is true, you should run [Self::shape_until_cursor] before redrawing. + /// Otherwise, you should run [Self::shape_until_scroll] + pub cursor_moved: bool, +} + +impl<'a> Editor<'a> { + pub fn new(buffer: Buffer<'a>) -> Self { + Self { + buffer, + cursor: Cursor::default(), + cursor_x_opt: None, + select_opt: None, + cursor_moved: false, + } + } + + 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_line = match layout.get(cursor.layout) { + Some(some) => some, + None => match layout.last() { + Some(some) => some, + None => todo!("layout cursor in line with no layouts"), + } + }; + + let new_index = match layout_line.glyphs.get(cursor.glyph) { + Some(glyph) => glyph.start, + None => match layout_line.glyphs.last() { + Some(glyph) => glyph.end, + //TODO: is this correct? + None => 0, + } + }; + + 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; + } + } + + /// Get the current cursor position + pub fn cursor(&self) -> Cursor { + self.cursor + } + + /// Perform a [Action] on the editor + pub fn action(&mut self, action: Action) { + let old_cursor = self.cursor; + + match action { + Action::Previous => { + let line = &mut self.buffer.lines[self.cursor.line]; + if self.cursor.index > 0 { + // Find previous character index + let mut prev_index = 0; + for (i, _) in line.text().grapheme_indices(true) { + if i < self.cursor.index { + prev_index = i; + } else { + break; + } + } + + self.cursor.index = prev_index; + self.buffer.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.cursor_x_opt = None; + }, + Action::Next => { + let line = &mut self.buffer.lines[self.cursor.line]; + if self.cursor.index < line.text().len() { + for (i, c) in line.text().grapheme_indices(true) { + if i == self.cursor.index { + self.cursor.index += c.len(); + self.buffer.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.cursor_x_opt = None; + }, + Action::Left => { + let rtl_opt = self.buffer.lines[self.cursor.line].shape_opt().as_ref().map(|shape| shape.rtl); + if let Some(rtl) = rtl_opt { + if rtl { + self.action(Action::Next); + } else { + self.action(Action::Previous); + } + } + }, + Action::Right => { + let rtl_opt = self.buffer.lines[self.cursor.line].shape_opt().as_ref().map(|shape| shape.rtl); + if let Some(rtl) = rtl_opt { + if rtl { + self.action(Action::Previous); + } else { + self.action(Action::Next); + } + } + }, + Action::Up => { + //TODO: make this preserve X as best as possible! + let mut cursor = self.buffer.layout_cursor(&self.cursor); + + if self.cursor_x_opt.is_none() { + self.cursor_x_opt = Some( + cursor.glyph as i32 //TODO: glyph x position + ); + } + + if cursor.layout > 0 { + cursor.layout -= 1; + } else if cursor.line > 0 { + cursor.line -= 1; + cursor.layout = usize::max_value(); + } + + if let Some(cursor_x) = self.cursor_x_opt { + cursor.glyph = cursor_x as usize; //TODO: glyph x position + } + + self.set_layout_cursor(cursor); + }, + Action::Down => { + //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(); + + if self.cursor_x_opt.is_none() { + self.cursor_x_opt = Some( + cursor.glyph as i32 //TODO: glyph x position + ); + } + + if cursor.layout + 1 < layout_len { + cursor.layout += 1; + } else if cursor.line + 1 < self.buffer.lines.len() { + cursor.line += 1; + cursor.layout = 0; + } + + if let Some(cursor_x) = self.cursor_x_opt { + cursor.glyph = cursor_x as usize; //TODO: glyph x position + } + + self.set_layout_cursor(cursor); + }, + Action::Home => { + let mut cursor = self.buffer.layout_cursor(&self.cursor); + cursor.glyph = 0; + self.set_layout_cursor(cursor); + self.cursor_x_opt = None; + }, + Action::End => { + let mut cursor = self.buffer.layout_cursor(&self.cursor); + cursor.glyph = usize::max_value(); + self.set_layout_cursor(cursor); + self.cursor_x_opt = None; + } + Action::PageUp => { + //TODO: move cursor + let mut scroll = self.buffer.scroll(); + scroll -= self.buffer.visible_lines(); + self.buffer.set_scroll(scroll); + }, + Action::PageDown => { + //TODO: move cursor + let mut scroll = self.buffer.scroll(); + scroll += self.buffer.visible_lines(); + self.buffer.set_scroll(scroll); + }, + Action::Insert(character) => { + if character.is_control() + && !['\t', '\u{92}'].contains(&character) + { + // Filter out special chars (except for tab), use Action instead + log::debug!("Refusing to insert control character {:?}", character); + } else { + let line = &mut self.buffer.lines[self.cursor.line]; + + println!("Before"); + for span in line.attrs_list().spans() { + println!("{:?}", span); + } + + // Collect text after insertion as a line + let after = line.split_off(self.cursor.index); + + // Append the inserted text + line.append(BufferLine::new( + character.to_string(), + AttrsList::new(line.attrs_list().defaults() /*TODO: provide attrs?*/) + )); + + // Append the text after insertion + line.append(after); + + println!("After"); + for span in line.attrs_list().spans() { + println!("{:?}", span); + } + + self.cursor.index += character.len_utf8(); + } + }, + Action::Enter => { + let new_line = self.buffer.lines[self.cursor.line].split_off(self.cursor.index); + + self.cursor.line += 1; + self.cursor.index = 0; + + self.buffer.lines.insert(self.cursor.line, new_line); + }, + Action::Backspace => { + if self.cursor.index > 0 { + let line = &mut self.buffer.lines[self.cursor.line]; + + println!("Before"); + for span in line.attrs_list().spans() { + println!("{:?}", span); + } + + // Get text line after cursor + let after = line.split_off(self.cursor.index); + + // Find previous character index + let mut prev_index = 0; + for (i, _) in line.text().char_indices() { + if i < self.cursor.index { + prev_index = i; + } else { + break; + } + } + + println!("Move cursor {} to {}", self.cursor.index, prev_index); + + self.cursor.index = prev_index; + + // Remove character + line.split_off(self.cursor.index); + + // Add text after cursor + line.append(after); + + println!("After"); + for span in line.attrs_list().spans() { + println!("{:?}", span); + } + } else if self.cursor.line > 0 { + let mut line_index = self.cursor.line; + let old_line = self.buffer.lines.remove(line_index); + line_index -= 1; + + let line = &mut self.buffer.lines[line_index]; + + self.cursor.line = line_index; + self.cursor.index = line.text().len(); + + line.append(old_line); + } + }, + Action::Delete => { + if self.cursor.index < self.buffer.lines[self.cursor.line].text().len() { + let line = &mut self.buffer.lines[self.cursor.line]; + + let range_opt = line + .text() + .grapheme_indices(true) + .take_while(|(i, _)| *i <= self.cursor.index) + .last() + .map(|(i, c)| { + i..(i + c.len()) + }); + + if let Some(range) = range_opt { + self.cursor.index = range.start; + + // Get text after deleted EGC + let after = line.split_off(range.end); + + // Delete EGC + line.split_off(range.start); + + // Add text after deleted EGC + line.append(after); + } + } else if self.cursor.line + 1 < self.buffer.lines.len() { + let old_line = self.buffer.lines.remove(self.cursor.line + 1); + self.buffer.lines[self.cursor.line].append(old_line); + } + }, + Action::Click { x, y } => { + self.select_opt = None; + + if let Some(new_cursor) = self.buffer.hit(x, y) { + if new_cursor != self.cursor { + self.cursor = new_cursor; + self.buffer.redraw = true; + } + } + }, + Action::Drag { x, y } => { + if self.select_opt.is_none() { + self.select_opt = Some(self.cursor); + self.buffer.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; + } + } + }, + Action::Scroll { lines } => { + let mut scroll = self.buffer.scroll(); + scroll += lines; + self.buffer.set_scroll(scroll); + } + } + + if old_cursor != self.cursor { + self.cursor_moved = true; + + /*TODO + if let Some(glyph) = run.glyphs.get(new_cursor_glyph) { + let font_opt = self.buffer.font_system().get_font(glyph.cache_key.font_id); + let text_glyph = &run.text[glyph.start..glyph.end]; + log::debug!( + "{}, {}: '{}' ('{}'): '{}' ({:?})", + self.cursor.line, + self.cursor.index, + font_opt.as_ref().map_or("?", |font| font.info.family.as_str()), + font_opt.as_ref().map_or("?", |font| font.info.post_script_name.as_str()), + text_glyph, + text_glyph + ); + } + */ + } + } + + /// Draw the editor + #[cfg(feature = "swash")] + pub fn draw(&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)> { + if cursor.line == line_i { + for (glyph_i, glyph) in run.glyphs.iter().enumerate() { + if cursor.index == glyph.start { + return Some((glyph_i, 0.0)); + } else 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 offset = glyph.w * (before as f32) / (total as f32); + return Some((glyph_i, offset)); + } + } + match run.glyphs.last() { + Some(glyph) => { + if cursor.index == glyph.end { + return Some((run.glyphs.len(), 0.0)); + } + }, + None => { + return Some((0, 0.0)); + } + } + } + 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_opt(&self.cursor) { + let 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 + } else { + (glyph.x + cursor_glyph_offset) as i32 + } + }, + None => match run.glyphs.last() { + Some(glyph) => { + // End of last glyph + if glyph.rtl { + glyph.x as i32 + } else { + (glyph.x + glyph.w) as i32 + } + }, + None => { + // Start of empty line + 0 + } + } + }; + + f( + 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) + }); + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs index cfb8a25..abd6501 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,11 +8,11 @@ //! 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 -//! [TextBuffer], provide it with some text, and then inspect the layout it produces. At this +//! [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. //! //! ``` -//! use cosmic_text::{Attrs, Color, FontSystem, SwashCache, TextBuffer, TextMetrics}; +//! use cosmic_text::{Attrs, Color, FontSystem, SwashCache, Buffer, Metrics}; //! //! // A FontSystem provides access to detected system fonts, create one per application //! let font_system = FontSystem::new(); @@ -21,25 +21,25 @@ //! let mut swash_cache = SwashCache::new(&font_system); //! //! // Text metrics indicate the font size and line height of a buffer -//! let metrics = TextMetrics::new(14, 20); +//! let metrics = Metrics::new(14, 20); //! -//! // A TextBuffer provides shaping and layout for a UTF-8 string, create one per text widget -//! let mut text_buffer = TextBuffer::new(&font_system, metrics); +//! // A Buffer provides shaping and layout for a UTF-8 string, create one per text widget +//! let mut buffer = Buffer::new(&font_system, metrics); //! //! // Set a size for the text buffer, in pixels -//! text_buffer.set_size(80, 25); +//! buffer.set_size(80, 25); //! //! // Attributes indicate what font to choose //! let attrs = Attrs::new(); //! //! // Add some text! -//! text_buffer.set_text("Hello, Rust! 🦀\n", attrs); +//! buffer.set_text("Hello, Rust! 🦀\n", attrs); //! //! // Perform shaping as desired -//! text_buffer.shape_until_scroll(); +//! buffer.shape_until_scroll(); //! //! // Inspect the output runs -//! for run in text_buffer.layout_runs() { +//! for run in buffer.layout_runs() { //! for glyph in run.glyphs.iter() { //! println!("{:#?}", glyph); //! } @@ -49,7 +49,7 @@ //! let text_color = Color::rgb(0xFF, 0xFF, 0xFF); //! //! // Draw the buffer (for perfomance, instead use SwashCache directly) -//! text_buffer.draw(&mut swash_cache, text_color, |x, y, w, h, color| { +//! buffer.draw(&mut swash_cache, text_color, |x, y, w, h, color| { //! // Fill in your code here for drawing rectangles //! }); //! ``` @@ -66,6 +66,9 @@ mod buffer_line; pub use self::cache::*; mod cache; +pub use self::editor::*; +mod editor; + pub use self::font::*; mod font;