From 018a2e9d2a3d5a333d8284f08dfcdecad0285a62 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Fri, 15 Dec 2023 09:13:14 -0700 Subject: [PATCH] Move cursor motions to new Motion enum, move handling to Buffer --- examples/editor-libcosmic/src/text_box.rs | 18 +- examples/editor-orbclient/src/main.rs | 30 +- examples/editor-test/src/main.rs | 6 +- examples/rich-text/src/main.rs | 32 +- src/buffer.rs | 393 ++++++++++++++++------ src/cursor.rs | 147 ++++++++ src/edit/editor.rs | 297 +--------------- src/edit/mod.rs | 46 +-- src/edit/vi.rs | 100 +++--- src/lib.rs | 3 + 10 files changed, 567 insertions(+), 505 deletions(-) create mode 100644 src/cursor.rs diff --git a/examples/editor-libcosmic/src/text_box.rs b/examples/editor-libcosmic/src/text_box.rs index b902e75..a3c950c 100644 --- a/examples/editor-libcosmic/src/text_box.rs +++ b/examples/editor-libcosmic/src/text_box.rs @@ -5,7 +5,7 @@ use cosmic::{ iced_runtime::keyboard::KeyCode, theme::{Theme, ThemeType}, }; -use cosmic_text::{Action, Edit, SwashCache}; +use cosmic_text::{Action, Edit, Motion, SwashCache}; use std::{cmp, sync::Mutex, time::Instant}; use crate::FONT_SYSTEM; @@ -281,35 +281,35 @@ where match event { Event::Keyboard(keyboard::Event::KeyPressed { key_code, .. }) => match key_code { KeyCode::Left => { - editor.action(Action::Left); + editor.action(Action::Motion(Motion::Left)); status = Status::Captured; } KeyCode::Right => { - editor.action(Action::Right); + editor.action(Action::Motion(Motion::Right)); status = Status::Captured; } KeyCode::Up => { - editor.action(Action::Up); + editor.action(Action::Motion(Motion::Up)); status = Status::Captured; } KeyCode::Down => { - editor.action(Action::Down); + editor.action(Action::Motion(Motion::Down)); status = Status::Captured; } KeyCode::Home => { - editor.action(Action::Home); + editor.action(Action::Motion(Motion::Home)); status = Status::Captured; } KeyCode::End => { - editor.action(Action::End); + editor.action(Action::Motion(Motion::End)); status = Status::Captured; } KeyCode::PageUp => { - editor.action(Action::PageUp); + editor.action(Action::Motion(Motion::PageUp)); status = Status::Captured; } KeyCode::PageDown => { - editor.action(Action::PageDown); + editor.action(Action::Motion(Motion::PageDown)); status = Status::Captured; } KeyCode::Escape => { diff --git a/examples/editor-orbclient/src/main.rs b/examples/editor-orbclient/src/main.rs index 14caf46..309f4a5 100644 --- a/examples/editor-orbclient/src/main.rs +++ b/examples/editor-orbclient/src/main.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 use cosmic_text::{ - Action, Attrs, Buffer, Edit, Family, FontSystem, Metrics, SwashCache, SyntaxEditor, + Action, Attrs, Buffer, Edit, Family, FontSystem, Metrics, Motion, SwashCache, SyntaxEditor, SyntaxSystem, }; use orbclient::{EventOption, Renderer, Window, WindowFlag}; @@ -147,14 +147,26 @@ 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 => 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_LEFT if event.pressed => { + editor.action(Action::Motion(Motion::Left)) + } + orbclient::K_RIGHT if event.pressed => { + editor.action(Action::Motion(Motion::Right)) + } + orbclient::K_UP if event.pressed => editor.action(Action::Motion(Motion::Up)), + orbclient::K_DOWN if event.pressed => { + editor.action(Action::Motion(Motion::Down)) + } + orbclient::K_HOME if event.pressed => { + editor.action(Action::Motion(Motion::Home)) + } + orbclient::K_END if event.pressed => editor.action(Action::Motion(Motion::End)), + orbclient::K_PGUP if event.pressed => { + editor.action(Action::Motion(Motion::PageUp)) + } + orbclient::K_PGDN if event.pressed => { + editor.action(Action::Motion(Motion::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), diff --git a/examples/editor-test/src/main.rs b/examples/editor-test/src/main.rs index 8f3effe..c8fd28a 100644 --- a/examples/editor-test/src/main.rs +++ b/examples/editor-test/src/main.rs @@ -2,7 +2,7 @@ use cosmic_text::{ Action, BidiParagraphs, BorrowedWithFontSystem, Buffer, Color, Edit, Editor, FontSystem, - Metrics, SwashCache, + Metrics, Motion, SwashCache, }; use orbclient::{EventOption, Renderer, Window, WindowFlag}; use std::{env, fs, process, time::Instant}; @@ -106,7 +106,7 @@ fn main() { // Test delete of EGC { let cursor = editor.cursor(); - editor.action(Action::Previous); + editor.action(Action::Motion(Motion::Previous)); editor.action(Action::Delete); for c in grapheme.chars() { editor.action(Action::Insert(c)); @@ -130,7 +130,7 @@ fn main() { { let cursor = editor.cursor(); editor.action(Action::Enter); - editor.action(Action::Previous); + editor.action(Action::Motion(Motion::Previous)); editor.action(Action::Delete); assert_eq!(cursor, editor.cursor()); } diff --git a/examples/rich-text/src/main.rs b/examples/rich-text/src/main.rs index a3d8c41..ec3501c 100644 --- a/examples/rich-text/src/main.rs +++ b/examples/rich-text/src/main.rs @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 use cosmic_text::{ - Action, Attrs, Buffer, Color, Edit, Editor, Family, FontSystem, Metrics, Shaping, Style, - SwashCache, Weight, + Action, Attrs, Buffer, Color, Edit, Editor, Family, FontSystem, Metrics, Motion, Shaping, + Style, SwashCache, Weight, }; use orbclient::{EventOption, Renderer, Window, WindowFlag}; use std::{ @@ -152,14 +152,26 @@ fn main() { for event in window.events() { match event.to_option() { EventOption::Key(event) => match event.scancode { - 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_LEFT if event.pressed => { + editor.action(Action::Motion(Motion::Left)) + } + orbclient::K_RIGHT if event.pressed => { + editor.action(Action::Motion(Motion::Right)) + } + orbclient::K_UP if event.pressed => editor.action(Action::Motion(Motion::Up)), + orbclient::K_DOWN if event.pressed => { + editor.action(Action::Motion(Motion::Down)) + } + orbclient::K_HOME if event.pressed => { + editor.action(Action::Motion(Motion::Home)) + } + orbclient::K_END if event.pressed => editor.action(Action::Motion(Motion::End)), + orbclient::K_PGUP if event.pressed => { + editor.action(Action::Motion(Motion::PageUp)) + } + orbclient::K_PGDN if event.pressed => { + editor.action(Action::Motion(Motion::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), diff --git a/src/buffer.rs b/src/buffer.rs index fc285b5..bf3e1f0 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -6,102 +6,11 @@ use core::{cmp, fmt}; use unicode_segmentation::UnicodeSegmentation; use crate::{ - Attrs, AttrsList, BidiParagraphs, BorrowedWithFontSystem, BufferLine, Color, FontSystem, - LayoutGlyph, LayoutLine, ShapeBuffer, ShapeLine, Shaping, Wrap, + Affinity, Attrs, AttrsList, BidiParagraphs, BorrowedWithFontSystem, BufferLine, Color, Cursor, + FontSystem, LayoutCursor, LayoutGlyph, LayoutLine, Motion, ShapeBuffer, ShapeLine, Shaping, + Wrap, }; -/// Current cursor location -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Ord, PartialOrd)] -pub struct Cursor { - /// Text line the cursor is on - pub line: usize, - /// First-byte-index of glyph at cursor (will insert behind this glyph) - pub index: usize, - /// Whether to associate the cursor with the run before it or the run after it if placed at the - /// boundary between two runs - pub affinity: Affinity, - /// Cursor color - pub color: Option, -} - -impl Cursor { - /// Create a new cursor - pub const fn new(line: usize, index: usize) -> Self { - Self::new_with_affinity(line, index, Affinity::Before) - } - - /// Create a new cursor, specifying the affinity - pub const fn new_with_affinity(line: usize, index: usize, affinity: Affinity) -> Self { - Self { - line, - index, - affinity, - color: None, - } - } - /// Create a new cursor, specifying the color - pub const fn new_with_color(line: usize, index: usize, color: Color) -> Self { - Self { - line, - index, - affinity: Affinity::Before, - color: Some(color), - } - } -} - -/// Whether to associate cursors placed at a boundary between runs with the run before or after it. -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd)] -pub enum Affinity { - #[default] - Before, - After, -} - -impl Affinity { - pub fn before(&self) -> bool { - *self == Self::Before - } - - pub fn after(&self) -> bool { - *self == Self::After - } - - pub fn from_before(before: bool) -> Self { - if before { - Self::Before - } else { - Self::After - } - } - - pub fn from_after(after: bool) -> Self { - if after { - Self::After - } else { - Self::Before - } - } -} - -/// The position of a cursor within a [`Buffer`]. -#[derive(Debug)] -pub struct LayoutCursor { - pub line: usize, - pub layout: usize, - pub glyph: usize, -} - -impl LayoutCursor { - pub fn new(line: usize, layout: usize, glyph: usize) -> Self { - Self { - line, - layout, - glyph, - } - } -} - /// A line of visible text for rendering #[derive(Debug)] pub struct LayoutRun<'a> { @@ -848,6 +757,297 @@ impl Buffer { new_cursor_opt } + /// Apply a [`Motion`] to a [`Cursor`] + pub fn cursor_motion( + &mut self, + font_system: &mut FontSystem, + mut cursor: Cursor, + motion: Motion, + ) -> Option { + match motion { + Motion::LayoutCursor(layout_cursor) => { + let layout = self.line_layout(font_system, layout_cursor.line)?; + + let layout_line = match layout.get(layout_cursor.layout) { + Some(some) => some, + None => match layout.last() { + Some(some) => some, + None => return None, + }, + }; + + let (new_index, new_affinity) = match layout_line.glyphs.get(layout_cursor.glyph) { + Some(glyph) => (glyph.start, Affinity::After), + None => match layout_line.glyphs.last() { + Some(glyph) => (glyph.end, Affinity::Before), + //TODO: is this correct? + None => (0, Affinity::After), + }, + }; + + if cursor.line != layout_cursor.line + || cursor.index != new_index + || cursor.affinity != new_affinity + { + cursor.line = layout_cursor.line; + cursor.index = new_index; + cursor.affinity = new_affinity; + } + } + Motion::Previous => { + let line = self.lines.get(cursor.line)?; + if cursor.index > 0 { + // Find previous character index + let mut prev_index = 0; + for (i, _) in line.text().grapheme_indices(true) { + if i < cursor.index { + prev_index = i; + } else { + break; + } + } + + cursor.index = prev_index; + cursor.affinity = Affinity::After; + } else if cursor.line > 0 { + cursor.line -= 1; + cursor.index = self.lines.get(cursor.line)?.text().len(); + cursor.affinity = Affinity::After; + } + cursor.x_opt = None; + } + Motion::Next => { + let line = self.lines.get(cursor.line)?; + if cursor.index < line.text().len() { + for (i, c) in line.text().grapheme_indices(true) { + if i == cursor.index { + cursor.index += c.len(); + cursor.affinity = Affinity::Before; + break; + } + } + } else if cursor.line + 1 < self.lines.len() { + cursor.line += 1; + cursor.index = 0; + cursor.affinity = Affinity::Before; + } + cursor.x_opt = None; + } + Motion::Left => { + let rtl_opt = self + .line_shape(font_system, cursor.line) + .map(|shape| shape.rtl); + if let Some(rtl) = rtl_opt { + if rtl { + cursor = self.cursor_motion(font_system, cursor, Motion::Next)?; + } else { + cursor = self.cursor_motion(font_system, cursor, Motion::Previous)?; + } + } + } + Motion::Right => { + let rtl_opt = self + .line_shape(font_system, cursor.line) + .map(|shape| shape.rtl); + if let Some(rtl) = rtl_opt { + if rtl { + cursor = self.cursor_motion(font_system, cursor, Motion::Previous)?; + } else { + cursor = self.cursor_motion(font_system, cursor, Motion::Next)?; + } + } + } + Motion::Up => { + //TODO: make this preserve X as best as possible! + let mut layout_cursor = self.layout_cursor(&cursor); + + if cursor.x_opt.is_none() { + cursor.x_opt = Some( + layout_cursor.glyph as i32, //TODO: glyph x position + ); + } + + if layout_cursor.layout > 0 { + layout_cursor.layout -= 1; + } else if layout_cursor.line > 0 { + layout_cursor.line -= 1; + layout_cursor.layout = usize::max_value(); + } + + if let Some(cursor_x) = cursor.x_opt { + layout_cursor.glyph = cursor_x as usize; //TODO: glyph x position + } + + cursor = + self.cursor_motion(font_system, cursor, Motion::LayoutCursor(layout_cursor))?; + } + Motion::Down => { + //TODO: make this preserve X as best as possible! + let mut layout_cursor = self.layout_cursor(&cursor); + + let layout_len = self + .line_layout(font_system, layout_cursor.line) + .expect("layout not found") + .len(); + + if cursor.x_opt.is_none() { + cursor.x_opt = Some( + layout_cursor.glyph as i32, //TODO: glyph x position + ); + } + + if layout_cursor.layout + 1 < layout_len { + layout_cursor.layout += 1; + } else if layout_cursor.line + 1 < self.lines.len() { + layout_cursor.line += 1; + layout_cursor.layout = 0; + } + + if let Some(cursor_x) = cursor.x_opt { + layout_cursor.glyph = cursor_x as usize; //TODO: glyph x position + } + + cursor = + self.cursor_motion(font_system, cursor, Motion::LayoutCursor(layout_cursor))?; + } + Motion::Home => { + let mut layout_cursor = self.layout_cursor(&cursor); + layout_cursor.glyph = 0; + cursor = + self.cursor_motion(font_system, cursor, Motion::LayoutCursor(layout_cursor))?; + cursor.x_opt = None; + } + Motion::SoftHome => { + let line = self.lines.get(cursor.line)?; + cursor.index = line + .text() + .char_indices() + .filter_map(|(i, c)| if c.is_whitespace() { None } else { Some(i) }) + .next() + .unwrap_or(0); + cursor.x_opt = None; + } + Motion::End => { + let mut layout_cursor = self.layout_cursor(&cursor); + layout_cursor.glyph = usize::max_value(); + cursor = + self.cursor_motion(font_system, cursor, Motion::LayoutCursor(layout_cursor))?; + cursor.x_opt = None; + } + Motion::ParagraphStart => { + cursor.index = 0; + cursor.x_opt = None; + } + Motion::ParagraphEnd => { + cursor.index = self.lines.get(cursor.line)?.text().len(); + cursor.x_opt = None; + } + Motion::PageUp => { + cursor = self.cursor_motion( + font_system, + cursor, + Motion::Vertical(-self.size().1 as i32), + )?; + } + Motion::PageDown => { + cursor = self.cursor_motion( + font_system, + cursor, + Motion::Vertical(self.size().1 as i32), + )?; + } + Motion::Vertical(px) => { + // TODO more efficient + let lines = px / self.metrics().line_height as i32; + match lines.cmp(&0) { + cmp::Ordering::Less => { + for _ in 0..-lines { + cursor = self.cursor_motion(font_system, cursor, Motion::Up)?; + } + } + cmp::Ordering::Greater => { + for _ in 0..lines { + cursor = self.cursor_motion(font_system, cursor, Motion::Down)?; + } + } + cmp::Ordering::Equal => {} + } + } + Motion::PreviousWord => { + let line = self.lines.get(cursor.line)?; + if cursor.index > 0 { + cursor.index = line + .text() + .unicode_word_indices() + .rev() + .map(|(i, _)| i) + .find(|&i| i < cursor.index) + .unwrap_or(0); + } else if cursor.line > 0 { + cursor.line -= 1; + cursor.index = self.lines.get(cursor.line)?.text().len(); + } + cursor.x_opt = None; + } + Motion::NextWord => { + let line = self.lines.get(cursor.line)?; + if cursor.index < line.text().len() { + cursor.index = line + .text() + .unicode_word_indices() + .map(|(i, word)| i + word.len()) + .find(|&i| i > cursor.index) + .unwrap_or(line.text().len()); + } else if cursor.line + 1 < self.lines.len() { + cursor.line += 1; + cursor.index = 0; + } + cursor.x_opt = None; + } + Motion::LeftWord => { + let rtl_opt = self + .line_shape(font_system, cursor.line) + .map(|shape| shape.rtl); + if let Some(rtl) = rtl_opt { + if rtl { + cursor = self.cursor_motion(font_system, cursor, Motion::NextWord)?; + } else { + cursor = self.cursor_motion(font_system, cursor, Motion::PreviousWord)?; + } + } + } + Motion::RightWord => { + let rtl_opt = self + .line_shape(font_system, cursor.line) + .map(|shape| shape.rtl); + if let Some(rtl) = rtl_opt { + if rtl { + cursor = self.cursor_motion(font_system, cursor, Motion::PreviousWord)?; + } else { + cursor = self.cursor_motion(font_system, cursor, Motion::NextWord)?; + } + } + } + Motion::BufferStart => { + cursor.line = 0; + cursor.index = 0; + cursor.x_opt = None; + } + Motion::BufferEnd => { + cursor.line = self.lines.len() - 1; + cursor.index = self.lines.get(cursor.line)?.text().len(); + cursor.x_opt = None; + } + Motion::GotoLine(line) => { + let mut layout_cursor = self.layout_cursor(&cursor); + layout_cursor.line = line; + cursor = + self.cursor_motion(font_system, cursor, Motion::LayoutCursor(layout_cursor))?; + } + } + Some(cursor) + } + /// Draw the buffer #[cfg(feature = "swash")] pub fn draw( @@ -972,6 +1172,11 @@ impl<'a> BorrowedWithFontSystem<'a, Buffer> { .set_rich_text(self.font_system, spans, default_attrs, shaping); } + /// Apply a [`Motion`] to a [`Cursor`] + pub fn cursor_motion(&mut self, cursor: Cursor, motion: Motion) -> Option { + self.inner.cursor_motion(self.font_system, cursor, motion) + } + /// Draw the buffer #[cfg(feature = "swash")] pub fn draw(&mut self, cache: &mut crate::SwashCache, color: Color, f: F) diff --git a/src/cursor.rs b/src/cursor.rs new file mode 100644 index 0000000..4ea5e93 --- /dev/null +++ b/src/cursor.rs @@ -0,0 +1,147 @@ +use crate::Color; + +/// Current cursor location +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Ord, PartialOrd)] +pub struct Cursor { + /// Text line the cursor is on + pub line: usize, + /// First-byte-index of glyph at cursor (will insert behind this glyph) + pub index: usize, + /// Whether to associate the cursor with the run before it or the run after it if placed at the + /// boundary between two runs + pub affinity: Affinity, + /// Cached X position used for up and down movement + pub x_opt: Option, + /// Cursor color + pub color: Option, +} + +impl Cursor { + /// Create a new cursor + pub const fn new(line: usize, index: usize) -> Self { + Self::new_with_affinity(line, index, Affinity::Before) + } + + /// Create a new cursor, specifying the affinity + pub const fn new_with_affinity(line: usize, index: usize, affinity: Affinity) -> Self { + Self { + line, + index, + affinity, + x_opt: None, + color: None, + } + } + /// Create a new cursor, specifying the color + pub const fn new_with_color(line: usize, index: usize, color: Color) -> Self { + Self { + line, + index, + affinity: Affinity::Before, + x_opt: None, + color: Some(color), + } + } +} + +/// Whether to associate cursors placed at a boundary between runs with the run before or after it. +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd)] +pub enum Affinity { + #[default] + Before, + After, +} + +impl Affinity { + pub fn before(&self) -> bool { + *self == Self::Before + } + + pub fn after(&self) -> bool { + *self == Self::After + } + + pub fn from_before(before: bool) -> Self { + if before { + Self::Before + } else { + Self::After + } + } + + pub fn from_after(after: bool) -> Self { + if after { + Self::After + } else { + Self::Before + } + } +} + +/// The position of a cursor within a [`Buffer`]. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct LayoutCursor { + pub line: usize, + pub layout: usize, + pub glyph: usize, +} + +impl LayoutCursor { + /// Create a new [`LayoutCursor`] + pub fn new(line: usize, layout: usize, glyph: usize) -> Self { + Self { + line, + layout, + glyph, + } + } +} + +/// A motion to perform on a [`Cursor`] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Motion { + /// Apply specific [`LayoutCursor`] + LayoutCursor(LayoutCursor), + /// 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 start of line, skipping whitespace + SoftHome, + /// Move cursor to end of line + End, + /// Move cursor to start of paragraph + ParagraphStart, + /// Move cursor to end of paragraph + ParagraphEnd, + /// Move cursor up one page + PageUp, + /// Move cursor down one page + PageDown, + /// Move cursor up or down by a number of pixels + Vertical(i32), + /// Move cursor to previous word boundary + PreviousWord, + /// Move cursor to next word boundary + NextWord, + /// Move cursor to next word boundary to the left + LeftWord, + /// Move cursor to next word boundary to the right + RightWord, + /// Move cursor to the start of the document + BufferStart, + /// Move cursor to the end of the document + BufferEnd, + /// Move cursor to specific line + GotoLine(usize), +} diff --git a/src/edit/editor.rs b/src/edit/editor.rs index 621790e..a02d073 100644 --- a/src/edit/editor.rs +++ b/src/edit/editor.rs @@ -8,8 +8,8 @@ use unicode_segmentation::UnicodeSegmentation; #[cfg(feature = "swash")] use crate::Color; use crate::{ - Action, Affinity, AttrsList, Buffer, BufferLine, Change, ChangeItem, Cursor, Edit, FontSystem, - LayoutCursor, Selection, Shaping, + Action, AttrsList, Buffer, BufferLine, Change, ChangeItem, Cursor, Edit, FontSystem, Selection, + Shaping, }; /// A wrapper of [`Buffer`] for easy editing @@ -39,40 +39,6 @@ impl Editor { change: None, } } - - fn set_layout_cursor(&mut self, font_system: &mut FontSystem, cursor: LayoutCursor) { - let layout = self - .buffer - .line_layout(font_system, cursor.line) - .expect("layout not found"); - - 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, new_affinity) = match layout_line.glyphs.get(cursor.glyph) { - Some(glyph) => (glyph.start, Affinity::After), - None => match layout_line.glyphs.last() { - Some(glyph) => (glyph.end, Affinity::Before), - //TODO: is this correct? - None => (0, Affinity::After), - }, - }; - - if self.cursor.line != cursor.line - || self.cursor.index != new_index - || self.cursor.affinity != new_affinity - { - self.cursor.line = cursor.line; - self.cursor.index = new_index; - self.cursor.affinity = new_affinity; - self.buffer.set_redraw(true); - } - } } impl Edit for Editor { @@ -377,181 +343,11 @@ impl Edit for Editor { let old_cursor = self.cursor; match action { - Action::Previous => { - let line = &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.cursor.affinity = Affinity::After; - 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.cursor.affinity = Affinity::After; - self.buffer.set_redraw(true); - } - self.cursor_x_opt = None; - } - Action::Next => { - let line = &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.cursor.affinity = Affinity::Before; - 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.cursor.affinity = Affinity::Before; - self.buffer.set_redraw(true); - } - self.cursor_x_opt = None; - } - Action::Left => { - let rtl_opt = self - .buffer - .line_shape(font_system, self.cursor.line) - .map(|shape| shape.rtl); - if let Some(rtl) = rtl_opt { - if rtl { - self.action(font_system, Action::Next); - } else { - self.action(font_system, Action::Previous); - } - } - } - Action::Right => { - let rtl_opt = self - .buffer - .line_shape(font_system, self.cursor.line) - .map(|shape| shape.rtl); - if let Some(rtl) = rtl_opt { - if rtl { - self.action(font_system, Action::Previous); - } else { - self.action(font_system, 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(font_system, 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(font_system, cursor.line) - .expect("layout not found") - .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(font_system, cursor); - } - Action::Home => { - let mut cursor = self.buffer.layout_cursor(&self.cursor); - cursor.glyph = 0; - self.set_layout_cursor(font_system, cursor); - self.cursor_x_opt = None; - } - Action::SoftHome => { - let line = &self.buffer.lines[self.cursor.line]; - self.cursor.index = line - .text() - .char_indices() - .filter_map(|(i, c)| if c.is_whitespace() { None } else { Some(i) }) - .next() - .unwrap_or(0); - self.buffer.set_redraw(true); - 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(font_system, cursor); - self.cursor_x_opt = None; - } - Action::ParagraphStart => { - self.cursor.index = 0; - self.cursor_x_opt = None; - self.buffer.set_redraw(true); - } - Action::ParagraphEnd => { - self.cursor.index = self.buffer.lines[self.cursor.line].text().len(); - self.cursor_x_opt = None; - self.buffer.set_redraw(true); - } - Action::PageUp => { - self.action(font_system, Action::Vertical(-self.buffer.size().1 as i32)); - } - Action::PageDown => { - self.action(font_system, Action::Vertical(self.buffer.size().1 as i32)); - } - Action::Vertical(px) => { - // TODO more efficient - let lines = px / self.buffer.metrics().line_height as i32; - match lines.cmp(&0) { - cmp::Ordering::Less => { - for _ in 0..-lines { - self.action(font_system, Action::Up); - } - } - cmp::Ordering::Greater => { - for _ in 0..lines { - self.action(font_system, Action::Down); - } - } - cmp::Ordering::Equal => {} + Action::Motion(motion) => { + if let Some(new_cursor) = + self.buffer.cursor_motion(font_system, self.cursor, motion) + { + self.cursor = new_cursor; } } Action::Escape => { @@ -837,88 +633,11 @@ impl Edit for Editor { scroll += lines; self.buffer.set_scroll(scroll); } - Action::PreviousWord => { - let line = &self.buffer.lines[self.cursor.line]; - if self.cursor.index > 0 { - self.cursor.index = line - .text() - .unicode_word_indices() - .rev() - .map(|(i, _)| i) - .find(|&i| i < self.cursor.index) - .unwrap_or(0); - - 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.set_redraw(true); - } - self.cursor_x_opt = None; - } - Action::NextWord => { - let line = &self.buffer.lines[self.cursor.line]; - if self.cursor.index < line.text().len() { - self.cursor.index = line - .text() - .unicode_word_indices() - .map(|(i, word)| i + word.len()) - .find(|&i| i > self.cursor.index) - .unwrap_or(line.text().len()); - - self.buffer.set_redraw(true); - } else if self.cursor.line + 1 < self.buffer.lines.len() { - self.cursor.line += 1; - self.cursor.index = 0; - self.buffer.set_redraw(true); - } - self.cursor_x_opt = None; - } - Action::LeftWord => { - let rtl_opt = self - .buffer - .line_shape(font_system, self.cursor.line) - .map(|shape| shape.rtl); - if let Some(rtl) = rtl_opt { - if rtl { - self.action(font_system, Action::NextWord); - } else { - self.action(font_system, Action::PreviousWord); - } - } - } - Action::RightWord => { - let rtl_opt = self - .buffer - .line_shape(font_system, self.cursor.line) - .map(|shape| shape.rtl); - if let Some(rtl) = rtl_opt { - if rtl { - self.action(font_system, Action::PreviousWord); - } else { - self.action(font_system, Action::NextWord); - } - } - } - Action::BufferStart => { - self.cursor.line = 0; - self.cursor.index = 0; - self.cursor_x_opt = None; - } - Action::BufferEnd => { - self.cursor.line = self.buffer.lines.len() - 1; - self.cursor.index = self.buffer.lines[self.cursor.line].text().len(); - self.cursor_x_opt = None; - } - Action::GotoLine(line) => { - let mut cursor = self.buffer.layout_cursor(&self.cursor); - cursor.line = line; - self.set_layout_cursor(font_system, cursor); - } } if old_cursor != self.cursor { self.cursor_moved = true; + self.buffer.set_redraw(true); /*TODO if let Some(glyph) = run.glyphs.get(new_cursor_glyph) { diff --git a/src/edit/mod.rs b/src/edit/mod.rs index 4975bfe..8c8c0c8 100644 --- a/src/edit/mod.rs +++ b/src/edit/mod.rs @@ -5,7 +5,7 @@ use unicode_segmentation::UnicodeSegmentation; #[cfg(feature = "swash")] use crate::Color; -use crate::{AttrsList, BorrowedWithFontSystem, Buffer, Cursor, FontSystem}; +use crate::{AttrsList, BorrowedWithFontSystem, Buffer, Cursor, FontSystem, Motion}; pub use self::editor::*; mod editor; @@ -23,34 +23,8 @@ 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 start of line, skipping whitespace - SoftHome, - /// Move cursor to end of line - End, - /// Move cursor to start of paragraph - ParagraphStart, - /// Move cursor to end of paragraph - ParagraphEnd, - /// Move cursor up one page - PageUp, - /// Move cursor down one page - PageDown, - /// Move cursor up or down by a number of pixels - Vertical(i32), + /// Move the cursor with some motion + Motion(Motion), /// Escape, clears selection Escape, /// Insert character at cursor @@ -89,20 +63,6 @@ pub enum Action { Scroll { lines: i32, }, - /// Move cursor to previous word boundary - PreviousWord, - /// Move cursor to next word boundary - NextWord, - /// Move cursor to next word boundary to the left - LeftWord, - /// Move cursor to next word boundary to the right - RightWord, - /// Move cursor to the start of the document - BufferStart, - /// Move cursor to the end of the document - BufferEnd, - /// Move cursor to specific line - GotoLine(usize), } /// A unique change to an editor diff --git a/src/edit/vi.rs b/src/edit/vi.rs index abaaf67..b95fe10 100644 --- a/src/edit/vi.rs +++ b/src/edit/vi.rs @@ -1,11 +1,11 @@ use alloc::{collections::BTreeMap, string::String}; use core::cmp; -use modit::{Event, Key, Motion, Parser, TextObject, WordIter}; +use modit::{Event, Key, Parser, TextObject, WordIter}; use unicode_segmentation::UnicodeSegmentation; use crate::{ Action, AttrsList, BorrowedWithFontSystem, Buffer, Change, Color, Cursor, Edit, FontSystem, - Selection, SyntaxEditor, SyntaxTheme, + Motion, Selection, SyntaxEditor, SyntaxTheme, }; pub use modit::{ViMode, ViParser}; @@ -342,19 +342,19 @@ impl<'a> Edit for ViEditor<'a> { //TODO: this leaves lots of room for issues in translation, should we directly accept Key? Action::Backspace => Key::Backspace, Action::Delete => Key::Delete, - Action::Down => Key::Down, - Action::End => Key::End, + Action::Motion(Motion::Down) => Key::Down, + Action::Motion(Motion::End) => Key::End, Action::Enter => Key::Enter, Action::Escape => Key::Escape, - Action::Home => Key::Home, + Action::Motion(Motion::Home) => Key::Home, Action::Indent => Key::Tab, Action::Insert(c) => Key::Char(c), - Action::Left => Key::Left, - Action::PageDown => Key::PageDown, - Action::PageUp => Key::PageUp, - Action::Right => Key::Right, + Action::Motion(Motion::Left) => Key::Left, + Action::Motion(Motion::PageDown) => Key::PageDown, + Action::Motion(Motion::PageUp) => Key::PageUp, + Action::Motion(Motion::Right) => Key::Right, Action::Unindent => Key::Backtab, - Action::Up => Key::Up, + Action::Motion(Motion::Up) => Key::Up, _ => { log::debug!("Pass through action {:?}", action); editor.action(font_system, action); @@ -448,9 +448,9 @@ impl<'a> Edit for ViEditor<'a> { // Move to inserted line, preserving cursor x position if after { - editor.action(font_system, Action::Down); + editor.action(font_system, Action::Motion(Motion::Down)); } else { - editor.action(font_system, Action::Up); + editor.action(font_system, Action::Motion(Motion::Up)); } } } @@ -550,35 +550,37 @@ impl<'a> Edit for ViEditor<'a> { } Event::Motion(motion) => { match motion { - Motion::Around => { + modit::Motion::Around => { //TODO: what to do for this psuedo-motion? return; } - Motion::Down => Action::Down, - Motion::End => Action::End, - Motion::GotoLine(line) => Action::GotoLine(line.saturating_sub(1)), - Motion::GotoEof => { - Action::GotoLine(editor.buffer().lines.len().saturating_sub(1)) + modit::Motion::Down => Action::Motion(Motion::Down), + modit::Motion::End => Action::Motion(Motion::End), + modit::Motion::GotoLine(line) => { + Action::Motion(Motion::GotoLine(line.saturating_sub(1))) } - Motion::Home => Action::Home, - Motion::Inside => { + modit::Motion::GotoEof => Action::Motion(Motion::GotoLine( + editor.buffer().lines.len().saturating_sub(1), + )), + modit::Motion::Home => Action::Motion(Motion::Home), + modit::Motion::Inside => { //TODO: what to do for this psuedo-motion? return; } - Motion::Left => Action::Left, - Motion::LeftInLine => { + modit::Motion::Left => Action::Motion(Motion::Left), + modit::Motion::LeftInLine => { let cursor = editor.cursor(); if cursor.index > 0 { - Action::Left + Action::Motion(Motion::Left) } else { return; } } - Motion::Line => { + modit::Motion::Line => { //TODO: what to do for this psuedo-motion? return; } - Motion::NextChar(find_c) => { + modit::Motion::NextChar(find_c) => { let mut cursor = editor.cursor(); let buffer = editor.buffer(); let text = buffer.lines[cursor.line].text(); @@ -597,7 +599,7 @@ impl<'a> Edit for ViEditor<'a> { } return; } - Motion::NextCharTill(find_c) => { + modit::Motion::NextCharTill(find_c) => { let mut cursor = editor.cursor(); let buffer = editor.buffer(); let text = buffer.lines[cursor.line].text(); @@ -615,14 +617,14 @@ impl<'a> Edit for ViEditor<'a> { } return; } - Motion::NextSearch => match &self.search_opt { + modit::Motion::NextSearch => match &self.search_opt { Some((value, forwards)) => { search(editor, value, *forwards); return; } None => return, }, - Motion::NextWordEnd(word) => { + modit::Motion::NextWordEnd(word) => { let mut cursor = editor.cursor(); let buffer = editor.buffer(); loop { @@ -649,7 +651,7 @@ impl<'a> Edit for ViEditor<'a> { editor.set_cursor(cursor); return; } - Motion::NextWordStart(word) => { + modit::Motion::NextWordStart(word) => { let mut cursor = editor.cursor(); let buffer = editor.buffer(); loop { @@ -674,9 +676,9 @@ impl<'a> Edit for ViEditor<'a> { editor.set_cursor(cursor); return; } - Motion::PageDown => Action::PageDown, - Motion::PageUp => Action::PageUp, - Motion::PreviousChar(find_c) => { + modit::Motion::PageDown => Action::Motion(Motion::PageDown), + modit::Motion::PageUp => Action::Motion(Motion::PageUp), + modit::Motion::PreviousChar(find_c) => { let mut cursor = editor.cursor(); let buffer = editor.buffer(); let text = buffer.lines[cursor.line].text(); @@ -695,7 +697,7 @@ impl<'a> Edit for ViEditor<'a> { } return; } - Motion::PreviousCharTill(find_c) => { + modit::Motion::PreviousCharTill(find_c) => { let mut cursor = editor.cursor(); let buffer = editor.buffer(); let text = buffer.lines[cursor.line].text(); @@ -722,14 +724,14 @@ impl<'a> Edit for ViEditor<'a> { } return; } - Motion::PreviousSearch => match &self.search_opt { + modit::Motion::PreviousSearch => match &self.search_opt { Some((value, forwards)) => { search(editor, value, !*forwards); return; } None => return, }, - Motion::PreviousWordEnd(word) => { + modit::Motion::PreviousWordEnd(word) => { let mut cursor = editor.cursor(); let buffer = editor.buffer(); loop { @@ -757,7 +759,7 @@ impl<'a> Edit for ViEditor<'a> { editor.set_cursor(cursor); return; } - Motion::PreviousWordStart(word) => { + modit::Motion::PreviousWordStart(word) => { let mut cursor = editor.cursor(); let buffer = editor.buffer(); loop { @@ -783,38 +785,40 @@ impl<'a> Edit for ViEditor<'a> { editor.set_cursor(cursor); return; } - Motion::Right => Action::Right, - Motion::RightInLine => { + modit::Motion::Right => Action::Motion(Motion::Right), + modit::Motion::RightInLine => { let cursor = editor.cursor(); let buffer = editor.buffer(); if cursor.index < buffer.lines[cursor.line].text().len() { - Action::Right + Action::Motion(Motion::Right) } else { return; } } - Motion::ScreenHigh => { + modit::Motion::ScreenHigh => { //TODO: is this efficient? if let Some(first) = editor.buffer().layout_runs().next() { - Action::GotoLine(first.line_i) + Action::Motion(Motion::GotoLine(first.line_i)) } else { return; } } - Motion::ScreenLow => { + modit::Motion::ScreenLow => { //TODO: is this efficient? if let Some(last) = editor.buffer().layout_runs().last() { - Action::GotoLine(last.line_i) + Action::Motion(Motion::GotoLine(last.line_i)) } else { return; } } - Motion::ScreenMiddle => { + modit::Motion::ScreenMiddle => { //TODO: is this efficient? let mut layout_runs = editor.buffer().layout_runs(); if let Some(first) = layout_runs.next() { if let Some(last) = layout_runs.last() { - Action::GotoLine((last.line_i + first.line_i) / 2) + Action::Motion(Motion::GotoLine( + (last.line_i + first.line_i) / 2, + )) } else { return; } @@ -822,12 +826,12 @@ impl<'a> Edit for ViEditor<'a> { return; } } - Motion::Selection => { + modit::Motion::Selection => { //TODO: what to do for this psuedo-motion? return; } - Motion::SoftHome => Action::SoftHome, - Motion::Up => Action::Up, + modit::Motion::SoftHome => Action::Motion(Motion::SoftHome), + modit::Motion::Up => Action::Motion(Motion::Up), } } }; diff --git a/src/lib.rs b/src/lib.rs index 2345697..7d5a120 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -111,6 +111,9 @@ mod buffer_line; pub use self::glyph_cache::*; mod glyph_cache; +pub use self::cursor::*; +mod cursor; + pub use self::edit::*; mod edit;