From 267485976b266550595110aa3ecdca32dcd964d4 Mon Sep 17 00:00:00 2001 From: valadaptive <79560998+valadaptive@users.noreply.github.com> Date: Thu, 20 Mar 2025 05:49:38 -0400 Subject: [PATCH] Simplify the text cursor API (#5785) * Closes N/A, but this is part of https://github.com/emilk/egui/issues/3378 * [x] I have followed the instructions in the PR template Other text layout libraries in Rust--namely, Parley and Cosmic Text--have one canonical text cursor type (Parley's is a byte index, Cosmic Text's also stores the line index). To prepare for migrating egui to one of those libraries, it should also have only one text cursor type. I also think simplifying the API is a good idea in and of itself--having three different cursor types that you have to convert between (and a `Cursor` struct which contains all three at once) is confusing. After a bit of experimentation, I found that the best cursor type to coalesce around is `CCursor`. In the few places where we need a paragraph index or row/column position, we can calculate them as necessary. I've removed `CursorRange` and `PCursorRange` (the latter appears to have never been used), merging the functionality with `CCursorRange`. To preserve the cursor position when navigating row-by-row, `CCursorRange` now stores the previous horizontal position of the cursor. I've also removed `PCursor`, and renamed `RowCursor` to `LayoutCursor` (since it includes not only the row but the column). I have not renamed either `CCursorRange` or `CCursor` as those names are used in a lot of places, and I don't want to clutter this PR with a bunch of renames. I'll leave it for a later PR. Finally, I've removed the deprecated methods from `TextEditState`--it made the refactoring easier, and it should be pretty easy to migrate to the equivalent `TextCursorState` methods. I'm not sure how many breaking changes people will actually encounter. A lot of these APIs were technically public, but I don't think many were useful. The `TextBuffer` trait now takes `&CCursorRange` instead of `&CursorRange` in a couple of methods, and I renamed `CCursorRange::sorted` to `CCursorRange::sorted_cursors` to match `CursorRange`. I did encounter a couple of apparent minor bugs when testing out text cursor behavior, but I checked them against the current version of egui and they're all pre-existing. --- crates/egui/src/lib.rs | 2 +- .../egui/src/text_selection/accesskit_text.rs | 8 +- .../egui/src/text_selection/cursor_range.rs | 308 +++++------- .../text_selection/label_text_selection.rs | 48 +- crates/egui/src/text_selection/mod.rs | 2 +- .../src/text_selection/text_cursor_state.rs | 92 +--- crates/egui/src/text_selection/visuals.rs | 8 +- crates/egui/src/widgets/text_edit/builder.rs | 62 ++- crates/egui/src/widgets/text_edit/output.rs | 4 +- crates/egui/src/widgets/text_edit/state.rs | 28 +- .../egui/src/widgets/text_edit/text_buffer.rs | 43 +- .../src/easy_mark/easy_mark_editor.rs | 4 +- crates/epaint/src/text/cursor.rs | 58 +-- crates/epaint/src/text/text_layout_types.rs | 457 ++++++------------ 14 files changed, 394 insertions(+), 730 deletions(-) diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index a6f46123..0d845980 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -471,7 +471,7 @@ pub use epaint::{ }; pub mod text { - pub use crate::text_selection::{CCursorRange, CursorRange}; + pub use crate::text_selection::CCursorRange; pub use epaint::text::{ cursor::CCursor, FontData, FontDefinitions, FontFamily, Fonts, Galley, LayoutJob, LayoutSection, TextFormat, TextWrapping, TAB_SIZE, diff --git a/crates/egui/src/text_selection/accesskit_text.rs b/crates/egui/src/text_selection/accesskit_text.rs index dedbc79d..d189498f 100644 --- a/crates/egui/src/text_selection/accesskit_text.rs +++ b/crates/egui/src/text_selection/accesskit_text.rs @@ -2,13 +2,13 @@ use emath::TSTransform; use crate::{Context, Galley, Id}; -use super::{text_cursor_state::is_word_char, CursorRange}; +use super::{text_cursor_state::is_word_char, CCursorRange}; /// Update accesskit with the current text state. pub fn update_accesskit_for_text_widget( ctx: &Context, widget_id: Id, - cursor_range: Option, + cursor_range: Option, role: accesskit::Role, global_from_galley: TSTransform, galley: &Galley, @@ -17,8 +17,8 @@ pub fn update_accesskit_for_text_widget( let parent_id = widget_id; if let Some(cursor_range) = &cursor_range { - let anchor = &cursor_range.secondary.rcursor; - let focus = &cursor_range.primary.rcursor; + let anchor = galley.layout_from_cursor(cursor_range.secondary); + let focus = galley.layout_from_cursor(cursor_range.primary); builder.set_text_selection(accesskit::TextSelection { anchor: accesskit::TextPosition { node: parent_id.with(anchor.row).accesskit_id(), diff --git a/crates/egui/src/text_selection/cursor_range.rs b/crates/egui/src/text_selection/cursor_range.rs index bd3f496f..05351e0a 100644 --- a/crates/egui/src/text_selection/cursor_range.rs +++ b/crates/egui/src/text_selection/cursor_range.rs @@ -1,41 +1,45 @@ -use epaint::{ - text::cursor::{CCursor, Cursor, PCursor}, - Galley, -}; +use epaint::{text::cursor::CCursor, Galley}; use crate::{os::OperatingSystem, Event, Id, Key, Modifiers}; use super::text_cursor_state::{ccursor_next_word, ccursor_previous_word, slice_char_range}; /// A selected text range (could be a range of length zero). +/// +/// The selection is based on character count (NOT byte count!). #[derive(Clone, Copy, Debug, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct CursorRange { +pub struct CCursorRange { /// When selecting with a mouse, this is where the mouse was released. /// When moving with e.g. shift+arrows, this is what moves. /// Note that the two ends can come in any order, and also be equal (no selection). - pub primary: Cursor, + pub primary: CCursor, /// When selecting with a mouse, this is where the mouse was first pressed. /// This part of the cursor does not move when shift is down. - pub secondary: Cursor, + pub secondary: CCursor, + + /// Saved horizontal position of the cursor. + pub h_pos: Option, } -impl CursorRange { +impl CCursorRange { /// The empty range. #[inline] - pub fn one(cursor: Cursor) -> Self { + pub fn one(ccursor: CCursor) -> Self { Self { - primary: cursor, - secondary: cursor, + primary: ccursor, + secondary: ccursor, + h_pos: None, } } #[inline] - pub fn two(min: Cursor, max: Cursor) -> Self { + pub fn two(min: impl Into, max: impl Into) -> Self { Self { - primary: max, - secondary: min, + primary: max.into(), + secondary: min.into(), + h_pos: None, } } @@ -44,39 +48,31 @@ impl CursorRange { Self::two(galley.begin(), galley.end()) } - pub fn as_ccursor_range(&self) -> CCursorRange { - CCursorRange { - primary: self.primary.ccursor, - secondary: self.secondary.ccursor, - } - } - /// The range of selected character indices. pub fn as_sorted_char_range(&self) -> std::ops::Range { let [start, end] = self.sorted_cursors(); std::ops::Range { - start: start.ccursor.index, - end: end.ccursor.index, + start: start.index, + end: end.index, } } /// True if the selected range contains no characters. #[inline] pub fn is_empty(&self) -> bool { - self.primary.ccursor == self.secondary.ccursor + self.primary == self.secondary } /// Is `self` a super-set of the other range? - pub fn contains(&self, other: &Self) -> bool { + pub fn contains(&self, other: Self) -> bool { let [self_min, self_max] = self.sorted_cursors(); let [other_min, other_max] = other.sorted_cursors(); - self_min.ccursor.index <= other_min.ccursor.index - && other_max.ccursor.index <= self_max.ccursor.index + self_min.index <= other_min.index && other_max.index <= self_max.index } /// If there is a selection, None is returned. /// If the two ends are the same, that is returned. - pub fn single(&self) -> Option { + pub fn single(&self) -> Option { if self.is_empty() { Some(self.primary) } else { @@ -84,25 +80,16 @@ impl CursorRange { } } + #[inline] pub fn is_sorted(&self) -> bool { - let p = self.primary.ccursor; - let s = self.secondary.ccursor; + let p = self.primary; + let s = self.secondary; (p.index, p.prefer_next_row) <= (s.index, s.prefer_next_row) } - pub fn sorted(self) -> Self { - if self.is_sorted() { - self - } else { - Self { - primary: self.secondary, - secondary: self.primary, - } - } - } - - /// Returns the two ends ordered. - pub fn sorted_cursors(&self) -> [Cursor; 2] { + /// returns the two ends ordered + #[inline] + pub fn sorted_cursors(&self) -> [CCursor; 2] { if self.is_sorted() { [self.primary, self.secondary] } else { @@ -110,9 +97,15 @@ impl CursorRange { } } + #[inline] + #[deprecated = "Use `self.sorted_cursors` instead."] + pub fn sorted(&self) -> [CCursor; 2] { + self.sorted_cursors() + } + pub fn slice_str<'s>(&self, text: &'s str) -> &'s str { let [min, max] = self.sorted_cursors(); - slice_char_range(text, min.ccursor.index..max.ccursor.index) + slice_char_range(text, min.index..max.index) } /// Check for key presses that are moving the cursor. @@ -146,7 +139,14 @@ impl CursorRange { | Key::ArrowDown | Key::Home | Key::End => { - move_single_cursor(os, &mut self.primary, galley, key, modifiers); + move_single_cursor( + os, + &mut self.primary, + &mut self.h_pos, + galley, + key, + modifiers, + ); if !modifiers.shift { self.secondary = self.primary; } @@ -156,7 +156,14 @@ impl CursorRange { Key::P | Key::N | Key::B | Key::F | Key::A | Key::E if os == OperatingSystem::Mac && modifiers.ctrl && !modifiers.shift => { - move_single_cursor(os, &mut self.primary, galley, key, modifiers); + move_single_cursor( + os, + &mut self.primary, + &mut self.h_pos, + galley, + key, + modifiers, + ); self.secondary = self.primary; true } @@ -196,8 +203,9 @@ impl CursorRange { ccursor_from_accesskit_text_position(_widget_id, galley, &selection.anchor); if let (Some(primary), Some(secondary)) = (primary, secondary) { *self = Self { - primary: galley.from_ccursor(primary), - secondary: galley.from_ccursor(secondary), + primary, + secondary, + h_pos: None, }; return true; } @@ -210,71 +218,6 @@ impl CursorRange { } } -/// A selected text range (could be a range of length zero). -/// -/// The selection is based on character count (NOT byte count!). -#[derive(Clone, Copy, Debug, Default, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct CCursorRange { - /// When selecting with a mouse, this is where the mouse was released. - /// When moving with e.g. shift+arrows, this is what moves. - /// Note that the two ends can come in any order, and also be equal (no selection). - pub primary: CCursor, - - /// When selecting with a mouse, this is where the mouse was first pressed. - /// This part of the cursor does not move when shift is down. - pub secondary: CCursor, -} - -impl CCursorRange { - /// The empty range. - #[inline] - pub fn one(ccursor: CCursor) -> Self { - Self { - primary: ccursor, - secondary: ccursor, - } - } - - #[inline] - pub fn two(min: impl Into, max: impl Into) -> Self { - Self { - primary: max.into(), - secondary: min.into(), - } - } - - #[inline] - pub fn is_sorted(&self) -> bool { - let p = self.primary; - let s = self.secondary; - (p.index, p.prefer_next_row) <= (s.index, s.prefer_next_row) - } - - /// returns the two ends ordered - #[inline] - pub fn sorted(&self) -> [CCursor; 2] { - if self.is_sorted() { - [self.primary, self.secondary] - } else { - [self.secondary, self.primary] - } - } -} - -#[derive(Clone, Copy, Debug, Default, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct PCursorRange { - /// When selecting with a mouse, this is where the mouse was released. - /// When moving with e.g. shift+arrows, this is what moves. - /// Note that the two ends can come in any order, and also be equal (no selection). - pub primary: PCursor, - - /// When selecting with a mouse, this is where the mouse was first pressed. - /// This part of the cursor does not move when shift is down. - pub secondary: PCursor, -} - // ---------------------------------------------------------------------------- #[cfg(feature = "accesskit")] @@ -304,78 +247,83 @@ fn ccursor_from_accesskit_text_position( /// Move a text cursor based on keyboard fn move_single_cursor( os: OperatingSystem, - cursor: &mut Cursor, + cursor: &mut CCursor, + h_pos: &mut Option, galley: &Galley, key: Key, modifiers: &Modifiers, ) { - if os == OperatingSystem::Mac && modifiers.ctrl && !modifiers.shift { - match key { - Key::A => *cursor = galley.cursor_begin_of_row(cursor), - Key::E => *cursor = galley.cursor_end_of_row(cursor), - Key::P => *cursor = galley.cursor_up_one_row(cursor), - Key::N => *cursor = galley.cursor_down_one_row(cursor), - Key::B => *cursor = galley.cursor_left_one_character(cursor), - Key::F => *cursor = galley.cursor_right_one_character(cursor), - _ => (), - } - return; - } - match key { - Key::ArrowLeft => { - if modifiers.alt || modifiers.ctrl { - // alt on mac, ctrl on windows - *cursor = galley.from_ccursor(ccursor_previous_word(galley, cursor.ccursor)); - } else if modifiers.mac_cmd { - *cursor = galley.cursor_begin_of_row(cursor); - } else { - *cursor = galley.cursor_left_one_character(cursor); + let (new_cursor, new_h_pos) = + if os == OperatingSystem::Mac && modifiers.ctrl && !modifiers.shift { + match key { + Key::A => (galley.cursor_begin_of_row(cursor), None), + Key::E => (galley.cursor_end_of_row(cursor), None), + Key::P => galley.cursor_up_one_row(cursor, *h_pos), + Key::N => galley.cursor_down_one_row(cursor, *h_pos), + Key::B => (galley.cursor_left_one_character(cursor), None), + Key::F => (galley.cursor_right_one_character(cursor), None), + _ => return, } - } - Key::ArrowRight => { - if modifiers.alt || modifiers.ctrl { - // alt on mac, ctrl on windows - *cursor = galley.from_ccursor(ccursor_next_word(galley, cursor.ccursor)); - } else if modifiers.mac_cmd { - *cursor = galley.cursor_end_of_row(cursor); - } else { - *cursor = galley.cursor_right_one_character(cursor); - } - } - Key::ArrowUp => { - if modifiers.command { - // mac and windows behavior - *cursor = galley.begin(); - } else { - *cursor = galley.cursor_up_one_row(cursor); - } - } - Key::ArrowDown => { - if modifiers.command { - // mac and windows behavior - *cursor = galley.end(); - } else { - *cursor = galley.cursor_down_one_row(cursor); - } - } + } else { + match key { + Key::ArrowLeft => { + if modifiers.alt || modifiers.ctrl { + // alt on mac, ctrl on windows + (ccursor_previous_word(galley, *cursor), None) + } else if modifiers.mac_cmd { + (galley.cursor_begin_of_row(cursor), None) + } else { + (galley.cursor_left_one_character(cursor), None) + } + } + Key::ArrowRight => { + if modifiers.alt || modifiers.ctrl { + // alt on mac, ctrl on windows + (ccursor_next_word(galley, *cursor), None) + } else if modifiers.mac_cmd { + (galley.cursor_end_of_row(cursor), None) + } else { + (galley.cursor_right_one_character(cursor), None) + } + } + Key::ArrowUp => { + if modifiers.command { + // mac and windows behavior + (galley.begin(), None) + } else { + galley.cursor_up_one_row(cursor, *h_pos) + } + } + Key::ArrowDown => { + if modifiers.command { + // mac and windows behavior + (galley.end(), None) + } else { + galley.cursor_down_one_row(cursor, *h_pos) + } + } - Key::Home => { - if modifiers.ctrl { - // windows behavior - *cursor = galley.begin(); - } else { - *cursor = galley.cursor_begin_of_row(cursor); - } - } - Key::End => { - if modifiers.ctrl { - // windows behavior - *cursor = galley.end(); - } else { - *cursor = galley.cursor_end_of_row(cursor); - } - } + Key::Home => { + if modifiers.ctrl { + // windows behavior + (galley.begin(), None) + } else { + (galley.cursor_begin_of_row(cursor), None) + } + } + Key::End => { + if modifiers.ctrl { + // windows behavior + (galley.end(), None) + } else { + (galley.cursor_end_of_row(cursor), None) + } + } - _ => unreachable!(), - } + _ => unreachable!(), + } + }; + + *cursor = new_cursor; + *h_pos = new_h_pos; } diff --git a/crates/egui/src/text_selection/label_text_selection.rs b/crates/egui/src/text_selection/label_text_selection.rs index aa9f0986..acd3db7d 100644 --- a/crates/egui/src/text_selection/label_text_selection.rs +++ b/crates/egui/src/text_selection/label_text_selection.rs @@ -10,7 +10,7 @@ use crate::{ use super::{ text_cursor_state::cursor_rect, visuals::{paint_text_selection, RowVertexIndices}, - CursorRange, TextCursorState, + TextCursorState, }; /// Turn on to help debug this @@ -44,7 +44,7 @@ impl WidgetTextCursor { } fn pos_in_galley(galley: &Galley, ccursor: CCursor) -> Pos2 { - galley.pos_from_ccursor(ccursor).center() + galley.pos_from_cursor(ccursor).center() } impl std::fmt::Debug for WidgetTextCursor { @@ -235,7 +235,7 @@ impl LabelSelectionState { self.selection = None; } - fn copy_text(&mut self, new_galley_rect: Rect, galley: &Galley, cursor_range: &CursorRange) { + fn copy_text(&mut self, new_galley_rect: Rect, galley: &Galley, cursor_range: &CCursorRange) { let new_text = selected_text(galley, cursor_range); if new_text.is_empty() { return; @@ -433,7 +433,11 @@ impl LabelSelectionState { match (primary, secondary) { (Some(primary), Some(secondary)) => { // This is the only selected label. - TextCursorState::from(CCursorRange { primary, secondary }) + TextCursorState::from(CCursorRange { + primary, + secondary, + h_pos: None, + }) } (Some(primary), None) => { @@ -442,12 +446,16 @@ impl LabelSelectionState { // Secondary was before primary. // Select everything up to the cursor. // We assume normal left-to-right and top-down layout order here. - galley.begin().ccursor + galley.begin() } else { // Select everything from the cursor onward: - galley.end().ccursor + galley.end() }; - TextCursorState::from(CCursorRange { primary, secondary }) + TextCursorState::from(CCursorRange { + primary, + secondary, + h_pos: None, + }) } (None, Some(secondary)) => { @@ -456,12 +464,16 @@ impl LabelSelectionState { // Primary was before secondary. // Select everything up to the cursor. // We assume normal left-to-right and top-down layout order here. - galley.begin().ccursor + galley.begin() } else { // Select everything from the cursor onward: - galley.end().ccursor + galley.end() }; - TextCursorState::from(CCursorRange { primary, secondary }) + TextCursorState::from(CCursorRange { + primary, + secondary, + h_pos: None, + }) } (None, None) => { @@ -515,7 +527,7 @@ impl LabelSelectionState { let mut cursor_state = self.cursor_for(ui, response, global_from_galley, galley); - let old_range = cursor_state.range(galley); + let old_range = cursor_state.char_range(); if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() { if response.contains_pointer() { @@ -529,7 +541,7 @@ impl LabelSelectionState { } } - if let Some(mut cursor_range) = cursor_state.range(galley) { + if let Some(mut cursor_range) = cursor_state.char_range() { let galley_rect = global_from_galley * Rect::from_min_size(Pos2::ZERO, galley.size()); self.selection_bbox_this_frame = self.selection_bbox_this_frame.union(galley_rect); @@ -543,11 +555,11 @@ impl LabelSelectionState { self.copy_text(galley_rect, galley, &cursor_range); } - cursor_state.set_range(Some(cursor_range)); + cursor_state.set_char_range(Some(cursor_range)); } // Look for changes due to keyboard and/or mouse interaction: - let new_range = cursor_state.range(galley); + let new_range = cursor_state.char_range(); let selection_changed = old_range != new_range; if let (true, Some(range)) = (selection_changed, new_range) { @@ -617,7 +629,7 @@ impl LabelSelectionState { } } - let cursor_range = cursor_state.range(galley); + let cursor_range = cursor_state.char_range(); let mut new_vertex_indices = vec![]; @@ -657,7 +669,7 @@ fn process_selection_key_events( ctx: &Context, galley: &Galley, widget_id: Id, - cursor_range: &mut CursorRange, + cursor_range: &mut CCursorRange, ) -> bool { let os = ctx.os(); @@ -674,10 +686,10 @@ fn process_selection_key_events( changed } -fn selected_text(galley: &Galley, cursor_range: &CursorRange) -> String { +fn selected_text(galley: &Galley, cursor_range: &CCursorRange) -> String { // This logic means we can select everything in an elided label (including the `…`) // and still copy the entire un-elided text! - let everything_is_selected = cursor_range.contains(&CursorRange::select_all(galley)); + let everything_is_selected = cursor_range.contains(CCursorRange::select_all(galley)); let copy_everything = cursor_range.is_empty() || everything_is_selected; diff --git a/crates/egui/src/text_selection/mod.rs b/crates/egui/src/text_selection/mod.rs index 5be95eb5..8d0943d6 100644 --- a/crates/egui/src/text_selection/mod.rs +++ b/crates/egui/src/text_selection/mod.rs @@ -8,6 +8,6 @@ mod label_text_selection; pub mod text_cursor_state; pub mod visuals; -pub use cursor_range::{CCursorRange, CursorRange, PCursorRange}; +pub use cursor_range::CCursorRange; pub use label_text_selection::LabelSelectionState; pub use text_cursor_state::TextCursorState; diff --git a/crates/egui/src/text_selection/text_cursor_state.rs b/crates/egui/src/text_selection/text_cursor_state.rs index ebc618b2..21ebda3d 100644 --- a/crates/egui/src/text_selection/text_cursor_state.rs +++ b/crates/egui/src/text_selection/text_cursor_state.rs @@ -1,13 +1,10 @@ //! Text cursor changes/interaction, without modifying the text. -use epaint::text::{ - cursor::{CCursor, Cursor}, - Galley, -}; +use epaint::text::{cursor::CCursor, Galley}; use crate::{epaint, NumExt, Rect, Response, Ui}; -use super::{CCursorRange, CursorRange}; +use super::CCursorRange; /// The state of a text cursor selection. /// @@ -16,29 +13,12 @@ use super::{CCursorRange, CursorRange}; #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] pub struct TextCursorState { - cursor_range: Option, - - /// This is what is easiest to work with when editing text, - /// so users are more likely to read/write this. ccursor_range: Option, } -impl From for TextCursorState { - fn from(cursor_range: CursorRange) -> Self { - Self { - cursor_range: Some(cursor_range), - ccursor_range: Some(CCursorRange { - primary: cursor_range.primary.ccursor, - secondary: cursor_range.secondary.ccursor, - }), - } - } -} - impl From for TextCursorState { fn from(ccursor_range: CCursorRange) -> Self { Self { - cursor_range: None, ccursor_range: Some(ccursor_range), } } @@ -46,50 +26,18 @@ impl From for TextCursorState { impl TextCursorState { pub fn is_empty(&self) -> bool { - self.cursor_range.is_none() && self.ccursor_range.is_none() + self.ccursor_range.is_none() } /// The currently selected range of characters. pub fn char_range(&self) -> Option { - self.ccursor_range.or_else(|| { - self.cursor_range - .map(|cursor_range| cursor_range.as_ccursor_range()) - }) - } - - pub fn range(&self, galley: &Galley) -> Option { - self.cursor_range - .map(|cursor_range| { - // We only use the PCursor (paragraph number, and character offset within that paragraph). - // This is so that if we resize the [`TextEdit`] region, and text wrapping changes, - // we keep the same byte character offset from the beginning of the text, - // even though the number of rows changes - // (each paragraph can be several rows, due to word wrapping). - // The column (character offset) should be able to extend beyond the last word so that we can - // go down and still end up on the same column when we return. - CursorRange { - primary: galley.from_pcursor(cursor_range.primary.pcursor), - secondary: galley.from_pcursor(cursor_range.secondary.pcursor), - } - }) - .or_else(|| { - self.ccursor_range.map(|ccursor_range| CursorRange { - primary: galley.from_ccursor(ccursor_range.primary), - secondary: galley.from_ccursor(ccursor_range.secondary), - }) - }) + self.ccursor_range } /// Sets the currently selected range of characters. pub fn set_char_range(&mut self, ccursor_range: Option) { - self.cursor_range = None; self.ccursor_range = ccursor_range; } - - pub fn set_range(&mut self, cursor_range: Option) { - self.cursor_range = cursor_range; - self.ccursor_range = None; - } } impl TextCursorState { @@ -100,7 +48,7 @@ impl TextCursorState { &mut self, ui: &Ui, response: &Response, - cursor_at_pointer: Cursor, + cursor_at_pointer: CCursor, galley: &Galley, is_being_dragged: bool, ) -> bool { @@ -108,39 +56,33 @@ impl TextCursorState { if response.double_clicked() { // Select word: - let ccursor_range = select_word_at(text, cursor_at_pointer.ccursor); - self.set_range(Some(CursorRange { - primary: galley.from_ccursor(ccursor_range.primary), - secondary: galley.from_ccursor(ccursor_range.secondary), - })); + let ccursor_range = select_word_at(text, cursor_at_pointer); + self.set_char_range(Some(ccursor_range)); true } else if response.triple_clicked() { // Select line: - let ccursor_range = select_line_at(text, cursor_at_pointer.ccursor); - self.set_range(Some(CursorRange { - primary: galley.from_ccursor(ccursor_range.primary), - secondary: galley.from_ccursor(ccursor_range.secondary), - })); + let ccursor_range = select_line_at(text, cursor_at_pointer); + self.set_char_range(Some(ccursor_range)); true } else if response.sense.senses_drag() { if response.hovered() && ui.input(|i| i.pointer.any_pressed()) { // The start of a drag (or a click). if ui.input(|i| i.modifiers.shift) { - if let Some(mut cursor_range) = self.range(galley) { + if let Some(mut cursor_range) = self.char_range() { cursor_range.primary = cursor_at_pointer; - self.set_range(Some(cursor_range)); + self.set_char_range(Some(cursor_range)); } else { - self.set_range(Some(CursorRange::one(cursor_at_pointer))); + self.set_char_range(Some(CCursorRange::one(cursor_at_pointer))); } } else { - self.set_range(Some(CursorRange::one(cursor_at_pointer))); + self.set_char_range(Some(CCursorRange::one(cursor_at_pointer))); } true } else if is_being_dragged { // Drag to select text: - if let Some(mut cursor_range) = self.range(galley) { + if let Some(mut cursor_range) = self.char_range() { cursor_range.primary = cursor_at_pointer; - self.set_range(Some(cursor_range)); + self.set_char_range(Some(cursor_range)); } true } else { @@ -336,8 +278,8 @@ pub fn slice_char_range(s: &str, char_range: std::ops::Range) -> &str { } /// The thin rectangle of one end of the selection, e.g. the primary cursor, in local galley coordinates. -pub fn cursor_rect(galley: &Galley, cursor: &Cursor, row_height: f32) -> Rect { - let mut cursor_pos = galley.pos_from_cursor(cursor); +pub fn cursor_rect(galley: &Galley, cursor: &CCursor, row_height: f32) -> Rect { + let mut cursor_pos = galley.pos_from_cursor(*cursor); // Handle completely empty galleys cursor_pos.max.y = cursor_pos.max.y.at_least(cursor_pos.min.y + row_height); diff --git a/crates/egui/src/text_selection/visuals.rs b/crates/egui/src/text_selection/visuals.rs index dd7c867a..4025a2d5 100644 --- a/crates/egui/src/text_selection/visuals.rs +++ b/crates/egui/src/text_selection/visuals.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use crate::{pos2, vec2, Galley, Painter, Rect, Ui, Visuals}; -use super::CursorRange; +use super::CCursorRange; #[derive(Clone, Debug)] pub struct RowVertexIndices { @@ -14,7 +14,7 @@ pub struct RowVertexIndices { pub fn paint_text_selection( galley: &mut Arc, visuals: &Visuals, - cursor_range: &CursorRange, + cursor_range: &CCursorRange, mut new_vertex_indices: Option<&mut Vec>, ) { if cursor_range.is_empty() { @@ -27,8 +27,8 @@ pub fn paint_text_selection( let color = visuals.selection.bg_fill; let [min, max] = cursor_range.sorted_cursors(); - let min = min.rcursor; - let max = max.rcursor; + let min = galley.layout_from_cursor(min); + let max = galley.layout_from_cursor(max); for ri in min.row..=max.row { let row = &mut galley.rows[ri]; diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 7d5c23f6..d73ecd3f 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -11,9 +11,7 @@ use crate::{ os::OperatingSystem, output::OutputEvent, response, text_selection, - text_selection::{ - text_cursor_state::cursor_rect, visuals::paint_text_selection, CCursorRange, CursorRange, - }, + text_selection::{text_cursor_state::cursor_rect, visuals::paint_text_selection, CCursorRange}, vec2, Align, Align2, Color32, Context, CursorIcon, Event, EventFilter, FontSelection, Id, ImeEvent, Key, KeyboardShortcut, Margin, Modifiers, NumExt, Response, Sense, Shape, TextBuffer, TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetWithState, @@ -614,14 +612,14 @@ impl TextEdit<'_> { } let mut cursor_range = None; - let prev_cursor_range = state.cursor.range(&galley); + let prev_cursor_range = state.cursor.char_range(); if interactive && ui.memory(|mem| mem.has_focus(id)) { ui.memory_mut(|mem| mem.set_focus_lock_filter(id, event_filter)); let default_cursor_range = if cursor_at_end { - CursorRange::one(galley.end()) + CCursorRange::one(galley.end()) } else { - CursorRange::default() + CCursorRange::default() }; let (changed, new_cursor_range) = events( @@ -655,7 +653,7 @@ impl TextEdit<'_> { // Visual clipping for singleline text editor with text larger than width if clip_text && align_offset == 0.0 { let cursor_pos = match (cursor_range, ui.memory(|mem| mem.has_focus(id))) { - (Some(cursor_range), true) => galley.pos_from_cursor(&cursor_range.primary).min.x, + (Some(cursor_range), true) => galley.pos_from_cursor(cursor_range.primary).min.x, _ => 0.0, }; @@ -683,7 +681,7 @@ impl TextEdit<'_> { let selection_changed = if let (Some(cursor_range), Some(prev_cursor_range)) = (cursor_range, prev_cursor_range) { - prev_cursor_range.as_ccursor_range() != cursor_range.as_ccursor_range() + prev_cursor_range != cursor_range } else { false }; @@ -717,7 +715,7 @@ impl TextEdit<'_> { let has_focus = ui.memory(|mem| mem.has_focus(id)); if has_focus { - if let Some(cursor_range) = state.cursor.range(&galley) { + if let Some(cursor_range) = state.cursor.char_range() { // Add text selection rectangles to the galley: paint_text_selection(&mut galley, ui.visuals(), &cursor_range, None); } @@ -739,7 +737,7 @@ impl TextEdit<'_> { painter.galley(galley_pos, galley.clone(), text_color); if has_focus { - if let Some(cursor_range) = state.cursor.range(&galley) { + if let Some(cursor_range) = state.cursor.char_range() { let primary_cursor_rect = cursor_rect(&galley, &cursor_range.primary, row_height) .translate(galley_pos.to_vec2()); @@ -809,8 +807,7 @@ impl TextEdit<'_> { }); } else if selection_changed { let cursor_range = cursor_range.unwrap(); - let char_range = - cursor_range.primary.ccursor.index..=cursor_range.secondary.ccursor.index; + let char_range = cursor_range.primary.index..=cursor_range.secondary.index; let info = WidgetInfo::text_selection_changed( ui.is_enabled(), char_range, @@ -887,20 +884,20 @@ fn events( wrap_width: f32, multiline: bool, password: bool, - default_cursor_range: CursorRange, + default_cursor_range: CCursorRange, char_limit: usize, event_filter: EventFilter, return_key: Option, -) -> (bool, CursorRange) { +) -> (bool, CCursorRange) { let os = ui.ctx().os(); - let mut cursor_range = state.cursor.range(galley).unwrap_or(default_cursor_range); + let mut cursor_range = state.cursor.char_range().unwrap_or(default_cursor_range); // We feed state to the undoer both before and after handling input // so that the undoer creates automatic saves even when there are no events for a while. state.undoer.lock().feed_state( ui.input(|i| i.time), - &(cursor_range.as_ccursor_range(), text.as_str().to_owned()), + &(cursor_range, text.as_str().to_owned()), ); let copy_if_not_password = |ui: &Ui, text: String| { @@ -1010,7 +1007,7 @@ fn events( if let Some((redo_ccursor_range, redo_txt)) = state .undoer .lock() - .redo(&(cursor_range.as_ccursor_range(), text.as_str().to_owned())) + .redo(&(cursor_range, text.as_str().to_owned())) { text.replace_with(redo_txt); Some(*redo_ccursor_range) @@ -1028,7 +1025,7 @@ fn events( if let Some((undo_ccursor_range, undo_txt)) = state .undoer .lock() - .undo(&(cursor_range.as_ccursor_range(), text.as_str().to_owned())) + .undo(&(cursor_range, text.as_str().to_owned())) { text.replace_with(undo_txt); Some(*undo_ccursor_range) @@ -1072,14 +1069,14 @@ fn events( state.ime_enabled = false; if !prediction.is_empty() - && cursor_range.secondary.ccursor.index - == state.ime_cursor_range.secondary.ccursor.index + && cursor_range.secondary.index + == state.ime_cursor_range.secondary.index { let mut ccursor = text.delete_selected(&cursor_range); text.insert_text_at(&mut ccursor, prediction, char_limit); Some(CCursorRange::one(ccursor)) } else { - let ccursor = cursor_range.primary.ccursor; + let ccursor = cursor_range.primary; Some(CCursorRange::one(ccursor)) } } @@ -1100,18 +1097,15 @@ fn events( *galley = layouter(ui, text.as_str(), wrap_width); // Set cursor_range using new galley: - cursor_range = CursorRange { - primary: galley.from_ccursor(new_ccursor_range.primary), - secondary: galley.from_ccursor(new_ccursor_range.secondary), - }; + cursor_range = new_ccursor_range; } } - state.cursor.set_range(Some(cursor_range)); + state.cursor.set_char_range(Some(cursor_range)); state.undoer.lock().feed_state( ui.input(|i| i.time), - &(cursor_range.as_ccursor_range(), text.as_str().to_owned()), + &(cursor_range, text.as_str().to_owned()), ); (any_change, cursor_range) @@ -1143,7 +1137,7 @@ fn remove_ime_incompatible_events(events: &mut Vec) { /// Returns `Some(new_cursor)` if we did mutate `text`. fn check_for_mutating_key_press( os: OperatingSystem, - cursor_range: &CursorRange, + cursor_range: &CCursorRange, text: &mut dyn TextBuffer, galley: &Galley, modifiers: &Modifiers, @@ -1156,9 +1150,9 @@ fn check_for_mutating_key_press( } else if let Some(cursor) = cursor_range.single() { if modifiers.alt || modifiers.ctrl { // alt on mac, ctrl on windows - text.delete_previous_word(cursor.ccursor) + text.delete_previous_word(cursor) } else { - text.delete_previous_char(cursor.ccursor) + text.delete_previous_char(cursor) } } else { text.delete_selected(cursor_range) @@ -1172,9 +1166,9 @@ fn check_for_mutating_key_press( } else if let Some(cursor) = cursor_range.single() { if modifiers.alt || modifiers.ctrl { // alt on mac, ctrl on windows - text.delete_next_word(cursor.ccursor) + text.delete_next_word(cursor) } else { - text.delete_next_char(cursor.ccursor) + text.delete_next_char(cursor) } } else { text.delete_selected(cursor_range) @@ -1187,7 +1181,7 @@ fn check_for_mutating_key_press( } Key::H if modifiers.ctrl => { - let ccursor = text.delete_previous_char(cursor_range.primary.ccursor); + let ccursor = text.delete_previous_char(cursor_range.primary); Some(CCursorRange::one(ccursor)) } @@ -1203,7 +1197,7 @@ fn check_for_mutating_key_press( Key::W if modifiers.ctrl => { let ccursor = if let Some(cursor) = cursor_range.single() { - text.delete_previous_word(cursor.ccursor) + text.delete_previous_word(cursor) } else { text.delete_selected(cursor_range) }; diff --git a/crates/egui/src/widgets/text_edit/output.rs b/crates/egui/src/widgets/text_edit/output.rs index d02c1d1c..2aa6711e 100644 --- a/crates/egui/src/widgets/text_edit/output.rs +++ b/crates/egui/src/widgets/text_edit/output.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use crate::text::CursorRange; +use crate::text::CCursorRange; /// The output from a [`TextEdit`](crate::TextEdit). pub struct TextEditOutput { @@ -20,7 +20,7 @@ pub struct TextEditOutput { pub state: super::TextEditState, /// Where the text cursor is. - pub cursor_range: Option, + pub cursor_range: Option, } impl TextEditOutput { diff --git a/crates/egui/src/widgets/text_edit/state.rs b/crates/egui/src/widgets/text_edit/state.rs index c10a8827..0734811b 100644 --- a/crates/egui/src/widgets/text_edit/state.rs +++ b/crates/egui/src/widgets/text_edit/state.rs @@ -3,8 +3,8 @@ use std::sync::Arc; use crate::mutex::Mutex; use crate::{ - text_selection::{CCursorRange, CursorRange, TextCursorState}, - Context, Galley, Id, + text_selection::{CCursorRange, TextCursorState}, + Context, Id, }; pub type TextEditUndoer = crate::util::undoer::Undoer<(CCursorRange, String)>; @@ -47,7 +47,7 @@ pub struct TextEditState { // cursor range for IME candidate. #[cfg_attr(feature = "serde", serde(skip))] - pub(crate) ime_cursor_range: CursorRange, + pub(crate) ime_cursor_range: CCursorRange, // Visual offset when editing singleline text bigger than the width. #[cfg_attr(feature = "serde", serde(skip))] @@ -68,23 +68,6 @@ impl TextEditState { ctx.data_mut(|d| d.insert_persisted(id, self)); } - /// The currently selected range of characters. - #[deprecated = "Use `self.cursor.char_range` instead"] - pub fn ccursor_range(&self) -> Option { - self.cursor.char_range() - } - - /// Sets the currently selected range of characters. - #[deprecated = "Use `self.cursor.set_char_range` instead"] - pub fn set_ccursor_range(&mut self, ccursor_range: Option) { - self.cursor.set_char_range(ccursor_range); - } - - #[deprecated = "Use `self.cursor.set_range` instead"] - pub fn set_cursor_range(&mut self, cursor_range: Option) { - self.cursor.set_range(cursor_range); - } - pub fn undoer(&self) -> TextEditUndoer { self.undoer.lock().clone() } @@ -97,9 +80,4 @@ impl TextEditState { pub fn clear_undoer(&mut self) { self.set_undoer(TextEditUndoer::default()); } - - #[deprecated = "Use `self.cursor.range` instead"] - pub fn cursor_range(&self, galley: &Galley) -> Option { - self.cursor.range(galley) - } } diff --git a/crates/egui/src/widgets/text_edit/text_buffer.rs b/crates/egui/src/widgets/text_edit/text_buffer.rs index 31b74632..9290f1e9 100644 --- a/crates/egui/src/widgets/text_edit/text_buffer.rs +++ b/crates/egui/src/widgets/text_edit/text_buffer.rs @@ -1,19 +1,16 @@ use std::{borrow::Cow, ops::Range}; use epaint::{ - text::{ - cursor::{CCursor, PCursor}, - TAB_SIZE, - }, + text::{cursor::CCursor, TAB_SIZE}, Galley, }; -use crate::text_selection::{ - text_cursor_state::{ +use crate::{ + text::CCursorRange, + text_selection::text_cursor_state::{ byte_index_from_char_index, ccursor_next_word, ccursor_previous_word, find_line_start, slice_char_range, }, - CursorRange, }; /// Trait constraining what types [`crate::TextEdit`] may use as @@ -111,9 +108,9 @@ pub trait TextBuffer { } } - fn delete_selected(&mut self, cursor_range: &CursorRange) -> CCursor { + fn delete_selected(&mut self, cursor_range: &CCursorRange) -> CCursor { let [min, max] = cursor_range.sorted_cursors(); - self.delete_selected_ccursor_range([min.ccursor, max.ccursor]) + self.delete_selected_ccursor_range([min, max]) } fn delete_selected_ccursor_range(&mut self, [min, max]: [CCursor; 2]) -> CCursor { @@ -151,36 +148,28 @@ pub trait TextBuffer { fn delete_paragraph_before_cursor( &mut self, galley: &Galley, - cursor_range: &CursorRange, + cursor_range: &CCursorRange, ) -> CCursor { let [min, max] = cursor_range.sorted_cursors(); - let min = galley.from_pcursor(PCursor { - paragraph: min.pcursor.paragraph, - offset: 0, - prefer_next_row: true, - }); - if min.ccursor == max.ccursor { - self.delete_previous_char(min.ccursor) + let min = galley.cursor_begin_of_paragraph(&min); + if min == max { + self.delete_previous_char(min) } else { - self.delete_selected(&CursorRange::two(min, max)) + self.delete_selected(&CCursorRange::two(min, max)) } } fn delete_paragraph_after_cursor( &mut self, galley: &Galley, - cursor_range: &CursorRange, + cursor_range: &CCursorRange, ) -> CCursor { let [min, max] = cursor_range.sorted_cursors(); - let max = galley.from_pcursor(PCursor { - paragraph: max.pcursor.paragraph, - offset: usize::MAX, // end of paragraph - prefer_next_row: false, - }); - if min.ccursor == max.ccursor { - self.delete_next_char(min.ccursor) + let max = galley.cursor_end_of_paragraph(&max); + if min == max { + self.delete_next_char(min) } else { - self.delete_selected(&CursorRange::two(min, max)) + self.delete_selected(&CCursorRange::two(min, max)) } } } diff --git a/crates/egui_demo_lib/src/easy_mark/easy_mark_editor.rs b/crates/egui_demo_lib/src/easy_mark/easy_mark_editor.rs index 524beaf6..9e730fac 100644 --- a/crates/egui_demo_lib/src/easy_mark/easy_mark_editor.rs +++ b/crates/egui_demo_lib/src/easy_mark/easy_mark_editor.rs @@ -146,7 +146,7 @@ fn shortcuts(ui: &Ui, code: &mut dyn TextBuffer, ccursor_range: &mut CCursorRang if ui.input_mut(|i| i.consume_shortcut(&SHORTCUT_INDENT)) { // This is a placeholder till we can indent the active line any_change = true; - let [primary, _secondary] = ccursor_range.sorted(); + let [primary, _secondary] = ccursor_range.sorted_cursors(); let advance = code.insert_text(" ", primary.index); ccursor_range.primary.index += advance; @@ -177,7 +177,7 @@ fn toggle_surrounding( ccursor_range: &mut CCursorRange, surrounding: &str, ) { - let [primary, secondary] = ccursor_range.sorted(); + let [primary, secondary] = ccursor_range.sorted_cursors(); let surrounding_ccount = surrounding.chars().count(); diff --git a/crates/epaint/src/text/cursor.rs b/crates/epaint/src/text/cursor.rs index 2158695e..a436ca1b 100644 --- a/crates/epaint/src/text/cursor.rs +++ b/crates/epaint/src/text/cursor.rs @@ -26,13 +26,6 @@ impl CCursor { } } -impl From for CCursor { - #[inline] - fn from(c: Cursor) -> Self { - c.ccursor - } -} - /// Two `CCursor`s are considered equal if they refer to the same character boundary, /// even if one prefers the start of the next row. impl PartialEq for CCursor { @@ -76,10 +69,12 @@ impl std::ops::SubAssign for CCursor { } } -/// Row Cursor +/// Row/column cursor. +/// +/// This refers to rows and columns in layout terms--text wrapping creates multiple rows. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct RCursor { +pub struct LayoutCursor { /// 0 is first row, and so on. /// Note that a single paragraph can span multiple rows. /// (a paragraph is text separated by `\n`). @@ -90,48 +85,3 @@ pub struct RCursor { /// When moving up/down it may again be within the next row. pub column: usize, } - -/// Paragraph Cursor -#[derive(Clone, Copy, Debug, Default)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct PCursor { - /// 0 is first paragraph, and so on. - /// Note that a single paragraph can span multiple rows. - /// (a paragraph is text separated by `\n`). - pub paragraph: usize, - - /// Character based (NOT bytes). - /// It is fine if this points to something beyond the end of the current paragraph. - /// When moving up/down it may again be within the next paragraph. - pub offset: usize, - - /// If this cursors sits right at the border of a wrapped row break (NOT paragraph break) - /// do we prefer the next row? - /// This is *almost* always what you want, *except* for when - /// explicitly clicking the end of a row or pressing the end key. - pub prefer_next_row: bool, -} - -/// Two `PCursor`s are considered equal if they refer to the same character boundary, -/// even if one prefers the start of the next row. -impl PartialEq for PCursor { - #[inline] - fn eq(&self, other: &Self) -> bool { - self.paragraph == other.paragraph && self.offset == other.offset - } -} - -/// All different types of cursors together. -/// -/// They all point to the same place, but in their own different ways. -/// pcursor/rcursor can also point to after the end of the paragraph/row. -/// Does not implement `PartialEq` because you must think which cursor should be equivalent. -/// -/// The default cursor is the zero-cursor, to the first character. -#[derive(Clone, Copy, Debug, Default, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct Cursor { - pub ccursor: CCursor, - pub rcursor: RCursor, - pub pcursor: PCursor, -} diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index b228d023..afe2573d 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -5,7 +5,7 @@ use std::ops::Range; use std::sync::Arc; use super::{ - cursor::{CCursor, Cursor, PCursor, RCursor}, + cursor::{CCursor, LayoutCursor}, font::UvRect, }; use crate::{Color32, FontId, Mesh, Stroke}; @@ -766,53 +766,18 @@ impl Galley { } /// Returns a 0-width Rect. - pub fn pos_from_cursor(&self, cursor: &Cursor) -> Rect { - self.pos_from_pcursor(cursor.pcursor) // pcursor is what TextEdit stores + fn pos_from_layout_cursor(&self, layout_cursor: &LayoutCursor) -> Rect { + let Some(row) = self.rows.get(layout_cursor.row) else { + return self.end_pos(); + }; + + let x = row.x_offset(layout_cursor.column); + Rect::from_min_max(pos2(x, row.min_y()), pos2(x, row.max_y())) } /// Returns a 0-width Rect. - pub fn pos_from_pcursor(&self, pcursor: PCursor) -> Rect { - let mut it = PCursor::default(); - - for row in &self.rows { - if it.paragraph == pcursor.paragraph { - // Right paragraph, but is it the right row in the paragraph? - - if it.offset <= pcursor.offset - && (pcursor.offset <= it.offset + row.char_count_excluding_newline() - || row.ends_with_newline) - { - let column = pcursor.offset - it.offset; - - let select_next_row_instead = pcursor.prefer_next_row - && !row.ends_with_newline - && column >= row.char_count_excluding_newline(); - if !select_next_row_instead { - let x = row.x_offset(column); - return Rect::from_min_max(pos2(x, row.min_y()), pos2(x, row.max_y())); - } - } - } - - if row.ends_with_newline { - it.paragraph += 1; - it.offset = 0; - } else { - it.offset += row.char_count_including_newline(); - } - } - - self.end_pos() - } - - /// Returns a 0-width Rect. - pub fn pos_from_ccursor(&self, ccursor: CCursor) -> Rect { - self.pos_from_cursor(&self.from_ccursor(ccursor)) - } - - /// Returns a 0-width Rect. - pub fn pos_from_rcursor(&self, rcursor: RCursor) -> Rect { - self.pos_from_cursor(&self.from_rcursor(rcursor)) + pub fn pos_from_cursor(&self, cursor: CCursor) -> Rect { + self.pos_from_layout_cursor(&self.layout_from_cursor(cursor)) } /// Cursor at the given position within the galley. @@ -822,7 +787,7 @@ impl Galley { /// and a cursor below the galley is considered /// same as a cursor at the end. /// This allows implementing text-selection by dragging above/below the galley. - pub fn cursor_from_pos(&self, pos: Vec2) -> Cursor { + pub fn cursor_from_pos(&self, pos: Vec2) -> CCursor { if let Some(first_row) = self.rows.first() { if pos.y < first_row.min_y() { return self.begin(); @@ -835,32 +800,20 @@ impl Galley { } let mut best_y_dist = f32::INFINITY; - let mut cursor = Cursor::default(); + let mut cursor = CCursor::default(); let mut ccursor_index = 0; - let mut pcursor_it = PCursor::default(); - for (row_nr, row) in self.rows.iter().enumerate() { + for row in &self.rows { let is_pos_within_row = row.min_y() <= pos.y && pos.y <= row.max_y(); let y_dist = (row.min_y() - pos.y).abs().min((row.max_y() - pos.y).abs()); if is_pos_within_row || y_dist < best_y_dist { best_y_dist = y_dist; let column = row.char_at(pos.x); let prefer_next_row = column < row.char_count_excluding_newline(); - cursor = Cursor { - ccursor: CCursor { - index: ccursor_index + column, - prefer_next_row, - }, - rcursor: RCursor { - row: row_nr, - column, - }, - pcursor: PCursor { - paragraph: pcursor_it.paragraph, - offset: pcursor_it.offset + column, - prefer_next_row, - }, + cursor = CCursor { + index: ccursor_index + column, + prefer_next_row, }; if is_pos_within_row { @@ -868,12 +821,6 @@ impl Galley { } } ccursor_index += row.char_count_including_newline(); - if row.ends_with_newline { - pcursor_it.paragraph += 1; - pcursor_it.offset = 0; - } else { - pcursor_it.offset += row.char_count_including_newline(); - } } cursor @@ -884,15 +831,15 @@ impl Galley { impl Galley { /// Cursor to the first character. /// - /// This is the same as [`Cursor::default`]. + /// This is the same as [`CCursor::default`]. #[inline] #[allow(clippy::unused_self)] - pub fn begin(&self) -> Cursor { - Cursor::default() + pub fn begin(&self) -> CCursor { + CCursor::default() } /// Cursor to one-past last character. - pub fn end(&self) -> Cursor { + pub fn end(&self) -> CCursor { if self.rows.is_empty() { return Default::default(); } @@ -900,31 +847,47 @@ impl Galley { index: 0, prefer_next_row: true, }; - let mut pcursor = PCursor { - paragraph: 0, - offset: 0, - prefer_next_row: true, - }; for row in &self.rows { let row_char_count = row.char_count_including_newline(); ccursor.index += row_char_count; - if row.ends_with_newline { - pcursor.paragraph += 1; - pcursor.offset = 0; - } else { - pcursor.offset += row_char_count; - } - } - Cursor { - ccursor, - rcursor: self.end_rcursor(), - pcursor, } + ccursor } +} + +/// ## Cursor conversions +impl Galley { + // The returned cursor is clamped. + pub fn layout_from_cursor(&self, cursor: CCursor) -> LayoutCursor { + let prefer_next_row = cursor.prefer_next_row; + let mut ccursor_it = CCursor { + index: 0, + prefer_next_row, + }; + + for (row_nr, row) in self.rows.iter().enumerate() { + let row_char_count = row.char_count_excluding_newline(); + + if ccursor_it.index <= cursor.index && cursor.index <= ccursor_it.index + row_char_count + { + let column = cursor.index - ccursor_it.index; + + let select_next_row_instead = prefer_next_row + && !row.ends_with_newline + && column >= row.char_count_excluding_newline(); + if !select_next_row_instead { + return LayoutCursor { + row: row_nr, + column, + }; + } + } + ccursor_it.index += row.char_count_including_newline(); + } + debug_assert!(ccursor_it == self.end()); - pub fn end_rcursor(&self) -> RCursor { if let Some(last_row) = self.rows.last() { - RCursor { + LayoutCursor { row: self.rows.len() - 1, column: last_row.char_count_including_newline(), } @@ -932,268 +895,156 @@ impl Galley { Default::default() } } -} -/// ## Cursor conversions -impl Galley { - // The returned cursor is clamped. - pub fn from_ccursor(&self, ccursor: CCursor) -> Cursor { - let prefer_next_row = ccursor.prefer_next_row; - let mut ccursor_it = CCursor { - index: 0, - prefer_next_row, - }; - let mut pcursor_it = PCursor { - paragraph: 0, - offset: 0, - prefer_next_row, - }; - - for (row_nr, row) in self.rows.iter().enumerate() { - let row_char_count = row.char_count_excluding_newline(); - - if ccursor_it.index <= ccursor.index - && ccursor.index <= ccursor_it.index + row_char_count - { - let column = ccursor.index - ccursor_it.index; - - let select_next_row_instead = prefer_next_row - && !row.ends_with_newline - && column >= row.char_count_excluding_newline(); - if !select_next_row_instead { - pcursor_it.offset += column; - return Cursor { - ccursor, - rcursor: RCursor { - row: row_nr, - column, - }, - pcursor: pcursor_it, - }; - } - } - ccursor_it.index += row.char_count_including_newline(); - if row.ends_with_newline { - pcursor_it.paragraph += 1; - pcursor_it.offset = 0; - } else { - pcursor_it.offset += row.char_count_including_newline(); - } - } - debug_assert!(ccursor_it == self.end().ccursor); - Cursor { - ccursor: ccursor_it, // clamp - rcursor: self.end_rcursor(), - pcursor: pcursor_it, - } - } - - pub fn from_rcursor(&self, rcursor: RCursor) -> Cursor { - if rcursor.row >= self.rows.len() { + fn cursor_from_layout(&self, layout_cursor: LayoutCursor) -> CCursor { + if layout_cursor.row >= self.rows.len() { return self.end(); } let prefer_next_row = - rcursor.column < self.rows[rcursor.row].char_count_excluding_newline(); - let mut ccursor_it = CCursor { + layout_cursor.column < self.rows[layout_cursor.row].char_count_excluding_newline(); + let mut cursor_it = CCursor { index: 0, prefer_next_row, }; - let mut pcursor_it = PCursor { - paragraph: 0, - offset: 0, - prefer_next_row, - }; for (row_nr, row) in self.rows.iter().enumerate() { - if row_nr == rcursor.row { - ccursor_it.index += rcursor.column.at_most(row.char_count_excluding_newline()); + if row_nr == layout_cursor.row { + cursor_it.index += layout_cursor + .column + .at_most(row.char_count_excluding_newline()); - if row.ends_with_newline { - // Allow offset to go beyond the end of the paragraph - pcursor_it.offset += rcursor.column; - } else { - pcursor_it.offset += rcursor.column.at_most(row.char_count_excluding_newline()); - } - return Cursor { - ccursor: ccursor_it, - rcursor, - pcursor: pcursor_it, - }; - } - ccursor_it.index += row.char_count_including_newline(); - if row.ends_with_newline { - pcursor_it.paragraph += 1; - pcursor_it.offset = 0; - } else { - pcursor_it.offset += row.char_count_including_newline(); + return cursor_it; } + cursor_it.index += row.char_count_including_newline(); } - Cursor { - ccursor: ccursor_it, - rcursor: self.end_rcursor(), - pcursor: pcursor_it, - } - } - - // TODO(emilk): return identical cursor, or clamp? - pub fn from_pcursor(&self, pcursor: PCursor) -> Cursor { - let prefer_next_row = pcursor.prefer_next_row; - let mut ccursor_it = CCursor { - index: 0, - prefer_next_row, - }; - let mut pcursor_it = PCursor { - paragraph: 0, - offset: 0, - prefer_next_row, - }; - - for (row_nr, row) in self.rows.iter().enumerate() { - if pcursor_it.paragraph == pcursor.paragraph { - // Right paragraph, but is it the right row in the paragraph? - - if pcursor_it.offset <= pcursor.offset - && (pcursor.offset <= pcursor_it.offset + row.char_count_excluding_newline() - || row.ends_with_newline) - { - let column = pcursor.offset - pcursor_it.offset; - - let select_next_row_instead = pcursor.prefer_next_row - && !row.ends_with_newline - && column >= row.char_count_excluding_newline(); - - if !select_next_row_instead { - ccursor_it.index += column.at_most(row.char_count_excluding_newline()); - - return Cursor { - ccursor: ccursor_it, - rcursor: RCursor { - row: row_nr, - column, - }, - pcursor, - }; - } - } - } - - ccursor_it.index += row.char_count_including_newline(); - if row.ends_with_newline { - pcursor_it.paragraph += 1; - pcursor_it.offset = 0; - } else { - pcursor_it.offset += row.char_count_including_newline(); - } - } - Cursor { - ccursor: ccursor_it, - rcursor: self.end_rcursor(), - pcursor, - } + cursor_it } } /// ## Cursor positions impl Galley { - pub fn cursor_left_one_character(&self, cursor: &Cursor) -> Cursor { - if cursor.ccursor.index == 0 { + #[allow(clippy::unused_self)] + pub fn cursor_left_one_character(&self, cursor: &CCursor) -> CCursor { + if cursor.index == 0 { Default::default() } else { - let ccursor = CCursor { - index: cursor.ccursor.index, - prefer_next_row: true, // default to this when navigating. It is more often useful to put cursor at the begging of a row than at the end. - }; - self.from_ccursor(ccursor - 1) + CCursor { + index: cursor.index - 1, + prefer_next_row: true, // default to this when navigating. It is more often useful to put cursor at the beginning of a row than at the end. + } } } - pub fn cursor_right_one_character(&self, cursor: &Cursor) -> Cursor { - let ccursor = CCursor { - index: cursor.ccursor.index, - prefer_next_row: true, // default to this when navigating. It is more often useful to put cursor at the begging of a row than at the end. - }; - self.from_ccursor(ccursor + 1) + pub fn cursor_right_one_character(&self, cursor: &CCursor) -> CCursor { + CCursor { + index: (cursor.index + 1).min(self.end().index), + prefer_next_row: true, // default to this when navigating. It is more often useful to put cursor at the beginning of a row than at the end. + } } - pub fn cursor_up_one_row(&self, cursor: &Cursor) -> Cursor { - if cursor.rcursor.row == 0 { - Cursor::default() + pub fn cursor_up_one_row( + &self, + cursor: &CCursor, + h_pos: Option, + ) -> (CCursor, Option) { + let layout_cursor = self.layout_from_cursor(*cursor); + let h_pos = h_pos.unwrap_or_else(|| self.pos_from_layout_cursor(&layout_cursor).center().x); + if layout_cursor.row == 0 { + (CCursor::default(), None) } else { - let new_row = cursor.rcursor.row - 1; + let new_row = layout_cursor.row - 1; - let cursor_is_beyond_end_of_current_row = cursor.rcursor.column - >= self.rows[cursor.rcursor.row].char_count_excluding_newline(); - - let new_rcursor = if cursor_is_beyond_end_of_current_row { - // keep same column - RCursor { - row: new_row, - column: cursor.rcursor.column, - } - } else { + let new_layout_cursor = { // keep same X coord - let x = self.pos_from_cursor(cursor).center().x; - let column = if x > self.rows[new_row].rect.right() { - // beyond the end of this row - keep same column - cursor.rcursor.column - } else { - self.rows[new_row].char_at(x) - }; - RCursor { + let column = self.rows[new_row].char_at(h_pos); + LayoutCursor { row: new_row, column, } }; - self.from_rcursor(new_rcursor) + (self.cursor_from_layout(new_layout_cursor), Some(h_pos)) } } - pub fn cursor_down_one_row(&self, cursor: &Cursor) -> Cursor { - if cursor.rcursor.row + 1 < self.rows.len() { - let new_row = cursor.rcursor.row + 1; + pub fn cursor_down_one_row( + &self, + cursor: &CCursor, + h_pos: Option, + ) -> (CCursor, Option) { + let layout_cursor = self.layout_from_cursor(*cursor); + let h_pos = h_pos.unwrap_or_else(|| self.pos_from_layout_cursor(&layout_cursor).center().x); + if layout_cursor.row + 1 < self.rows.len() { + let new_row = layout_cursor.row + 1; - let cursor_is_beyond_end_of_current_row = cursor.rcursor.column - >= self.rows[cursor.rcursor.row].char_count_excluding_newline(); - - let new_rcursor = if cursor_is_beyond_end_of_current_row { - // keep same column - RCursor { - row: new_row, - column: cursor.rcursor.column, - } - } else { + let new_layout_cursor = { // keep same X coord - let x = self.pos_from_cursor(cursor).center().x; - let column = if x > self.rows[new_row].rect.right() { - // beyond the end of the next row - keep same column - cursor.rcursor.column - } else { - self.rows[new_row].char_at(x) - }; - RCursor { + let column = self.rows[new_row].char_at(h_pos); + LayoutCursor { row: new_row, column, } }; - self.from_rcursor(new_rcursor) + (self.cursor_from_layout(new_layout_cursor), Some(h_pos)) } else { - self.end() + (self.end(), None) } } - pub fn cursor_begin_of_row(&self, cursor: &Cursor) -> Cursor { - self.from_rcursor(RCursor { - row: cursor.rcursor.row, + pub fn cursor_begin_of_row(&self, cursor: &CCursor) -> CCursor { + let layout_cursor = self.layout_from_cursor(*cursor); + self.cursor_from_layout(LayoutCursor { + row: layout_cursor.row, column: 0, }) } - pub fn cursor_end_of_row(&self, cursor: &Cursor) -> Cursor { - self.from_rcursor(RCursor { - row: cursor.rcursor.row, - column: self.rows[cursor.rcursor.row].char_count_excluding_newline(), + pub fn cursor_end_of_row(&self, cursor: &CCursor) -> CCursor { + let layout_cursor = self.layout_from_cursor(*cursor); + self.cursor_from_layout(LayoutCursor { + row: layout_cursor.row, + column: self.rows[layout_cursor.row].char_count_excluding_newline(), }) } + + pub fn cursor_begin_of_paragraph(&self, cursor: &CCursor) -> CCursor { + let mut layout_cursor = self.layout_from_cursor(*cursor); + layout_cursor.column = 0; + + loop { + let prev_row = layout_cursor + .row + .checked_sub(1) + .and_then(|row| self.rows.get(row)); + + let Some(prev_row) = prev_row else { + // This is the first row + break; + }; + + if prev_row.ends_with_newline { + break; + } + + layout_cursor.row -= 1; + } + + self.cursor_from_layout(layout_cursor) + } + + pub fn cursor_end_of_paragraph(&self, cursor: &CCursor) -> CCursor { + let mut layout_cursor = self.layout_from_cursor(*cursor); + loop { + let row = &self.rows[layout_cursor.row]; + if row.ends_with_newline || layout_cursor.row == self.rows.len() - 1 { + layout_cursor.column = row.char_count_excluding_newline(); + break; + } + + layout_cursor.row += 1; + } + + self.cursor_from_layout(layout_cursor) + } }