From 63e9eeffb5d7563a81e4c0d3a299909f2e7eab4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 1 Dec 2025 19:40:36 +0100 Subject: [PATCH 1/3] Refactor `cursor` API in `Editor` Co-authored-by: Neeraj Jaiswal --- core/src/renderer/null.rs | 11 +++++--- core/src/text/editor.rs | 31 +++++++++++++++++------ examples/editor/src/main.rs | 8 ++++-- graphics/src/text/editor.rs | 50 ++++++++++++++++++++++++++----------- widget/src/text_editor.rs | 42 ++++++++++++++++--------------- 5 files changed, 95 insertions(+), 47 deletions(-) diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index 45d10d72..71bd1682 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -166,14 +166,17 @@ impl text::Editor for () { } fn cursor(&self) -> text::editor::Cursor { - text::editor::Cursor::Caret(Point::ORIGIN) + text::editor::Cursor { + position: text::editor::Position { line: 0, column: 0 }, + selection: None, + } } - fn cursor_position(&self) -> (usize, usize) { - (0, 0) + fn selection(&self) -> text::editor::Selection { + text::editor::Selection::Caret(Point::ORIGIN) } - fn selection(&self) -> Option { + fn copy(&self) -> Option { None } diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs index bee5560d..7b31091b 100644 --- a/core/src/text/editor.rs +++ b/core/src/text/editor.rs @@ -20,13 +20,11 @@ pub trait Editor: Sized + Default { /// Returns the current [`Cursor`] of the [`Editor`]. fn cursor(&self) -> Cursor; - /// Returns the current cursor position of the [`Editor`]. - /// - /// Line and column, respectively. - fn cursor_position(&self) -> (usize, usize); + /// Returns the current [`Selection`] of the [`Editor`]. + fn selection(&self) -> Selection; /// Returns the current selected text of the [`Editor`]. - fn selection(&self) -> Option; + fn copy(&self) -> Option; /// Returns the text of the given line in the [`Editor`], if it exists. fn line(&self, index: usize) -> Option>; @@ -187,12 +185,31 @@ pub enum Direction { /// The cursor of an [`Editor`]. #[derive(Debug, Clone)] -pub enum Cursor { +pub enum Selection { /// Cursor without a selection Caret(Point), /// Cursor selecting a range of text - Selection(Vec), + Range(Vec), +} + +/// The range of an [`Editor`]. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Cursor { + /// The cursor position. + pub position: Position, + + /// The selection position, if any. + pub selection: Option, +} + +/// A cursor position in an [`Editor`]. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Position { + /// The line of text. + pub line: usize, + /// The column in the line. + pub column: usize, } /// A line of an [`Editor`]. diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 87546657..3bb657f5 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -190,9 +190,13 @@ impl Editor { }), space::horizontal(), text({ - let (line, column) = self.content.cursor_position(); + let cursor = self.content.cursor(); - format!("{}:{}", line + 1, column + 1) + format!( + "{}:{}", + cursor.position.line + 1, + cursor.position.column + 1 + ) }) ] .spacing(10); diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 0051c109..0114d504 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -1,6 +1,6 @@ //! Draw and edit text. use crate::core::text::editor::{ - self, Action, Cursor, Direction, Edit, Motion, + self, Action, Cursor, Direction, Edit, Motion, Position, Selection, }; use crate::core::text::highlighter::{self, Highlighter}; use crate::core::text::{LineHeight, Wrapping}; @@ -19,7 +19,7 @@ pub struct Editor(Option>); struct Internal { editor: cosmic_text::Editor<'static>, - cursor: RwLock>, + selection: RwLock>, font: Font, bounds: Size, topmost_line_changed: Option, @@ -109,14 +109,14 @@ impl editor::Editor for Editor { self.buffer().lines.len() } - fn selection(&self) -> Option { + fn copy(&self) -> Option { self.internal().editor.copy_selection() } - fn cursor(&self) -> editor::Cursor { + fn selection(&self) -> editor::Selection { let internal = self.internal(); - if let Ok(Some(cursor)) = internal.cursor.read().as_deref() { + if let Ok(Some(cursor)) = internal.selection.read().as_deref() { return cursor.clone(); } @@ -166,7 +166,7 @@ impl editor::Editor for Editor { }) .collect(); - Cursor::Selection(regions) + Selection::Range(regions) } _ => { let line_height = buffer.metrics().line_height; @@ -234,7 +234,7 @@ impl editor::Editor for Editor { layout.last().map(|line| line.w).unwrap_or(0.0), )); - Cursor::Caret(Point::new( + Selection::Caret(Point::new( offset, (visual_lines_offset + visual_line as i32) as f32 * line_height @@ -243,16 +243,38 @@ impl editor::Editor for Editor { } }; - *internal.cursor.write().expect("Write to cursor cache") = + *internal.selection.write().expect("Write to cursor cache") = Some(cursor.clone()); cursor } - fn cursor_position(&self) -> (usize, usize) { - let cursor = self.internal().editor.cursor(); + fn cursor(&self) -> Cursor { + let editor = &self.internal().editor; - (cursor.line, cursor.index) + let position = { + let cursor = editor.cursor(); + + Position { + line: cursor.line, + column: cursor.index, + } + }; + + let selection = match editor.selection() { + cosmic_text::Selection::None => None, + cosmic_text::Selection::Normal(cursor) + | cosmic_text::Selection::Line(cursor) + | cosmic_text::Selection::Word(cursor) => Some(Position { + line: cursor.line, + column: cursor.index, + }), + }; + + Cursor { + position, + selection, + } } fn perform(&mut self, action: Action) { @@ -270,7 +292,7 @@ impl editor::Editor for Editor { // Clear cursor cache let _ = internal - .cursor + .selection .write() .expect("Write to cursor cache") .take(); @@ -572,7 +594,7 @@ impl editor::Editor for Editor { // Clear cursor cache let _ = internal - .cursor + .selection .write() .expect("Write to cursor cache") .take(); @@ -685,7 +707,7 @@ impl Default for Internal { line_height: 1.0, }, )), - cursor: RwLock::new(None), + selection: RwLock::new(None), font: Font::default(), bounds: Size::ZERO, topmost_line_changed: None, diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index f7de779c..751603df 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -55,11 +55,13 @@ use crate::core::{ use std::borrow::Cow; use std::cell::RefCell; use std::fmt; +use std::ops; use std::ops::DerefMut; -use std::ops::Range; use std::sync::Arc; -pub use text::editor::{Action, Edit, Line, LineEnding, Motion}; +pub use text::editor::{ + Action, Edit, Line, LineEnding, Motion, Position, Selection, +}; /// A multi-line text input. /// @@ -354,9 +356,9 @@ where let text_bounds = bounds.shrink(self.padding); let translation = text_bounds.position() - Point::ORIGIN; - let cursor = match internal.editor.cursor() { - Cursor::Caret(position) => position, - Cursor::Selection(ranges) => { + let cursor = match internal.editor.selection() { + Selection::Caret(position) => position, + Selection::Range(ranges) => { ranges.first().cloned().unwrap_or_default().position() } }; @@ -416,6 +418,11 @@ where internal.is_dirty = true; } + /// Returns the current cursor position of the [`Content`]. + pub fn cursor(&self) -> Cursor { + self.0.borrow().editor.cursor() + } + /// Returns the amount of lines of the [`Content`]. pub fn line_count(&self) -> usize { self.0.borrow().editor.line_count() @@ -460,21 +467,16 @@ where contents } + /// Returns the selected text of the [`Content`]. + pub fn selection(&self) -> Option { + self.0.borrow().editor.copy() + } + /// Returns the kind of [`LineEnding`] used for separating lines in the [`Content`]. pub fn line_ending(&self) -> Option { Some(self.line(0)?.ending) } - /// Returns the selected text of the [`Content`]. - pub fn selection(&self) -> Option { - self.0.borrow().editor.selection() - } - - /// Returns the current cursor position of the [`Content`]. - pub fn cursor_position(&self) -> (usize, usize) { - self.0.borrow().editor.cursor_position() - } - /// Returns whether or not the the [`Content`] is empty. pub fn is_empty(&self) -> bool { self.0.borrow().editor.is_empty() @@ -1014,8 +1016,8 @@ where let translation = text_bounds.position() - Point::ORIGIN; if let Some(focus) = state.focus.as_ref() { - match internal.editor.cursor() { - Cursor::Caret(position) if focus.is_cursor_visible() => { + match internal.editor.selection() { + Selection::Caret(position) if focus.is_cursor_visible() => { let cursor = Rectangle::new( position + translation, @@ -1041,7 +1043,7 @@ where ); } } - Cursor::Selection(ranges) => { + Selection::Range(ranges) => { for range in ranges.into_iter().filter_map(|range| { text_bounds.intersection(&(range + translation)) }) { @@ -1054,7 +1056,7 @@ where ); } } - Cursor::Caret(_) => {} + Selection::Caret(_) => {} } } } @@ -1265,7 +1267,7 @@ enum Ime { Toggle(bool), Preedit { content: String, - selection: Option>, + selection: Option>, }, Commit(String), } From 4428f31b4f8a8282f3ec88f39d296d9ebe381d84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 1 Dec 2025 20:11:42 +0100 Subject: [PATCH 2/3] Add `move_to` method to `Editor` --- core/src/renderer/null.rs | 2 + core/src/text/editor.rs | 3 + graphics/src/text/editor.rs | 537 +++++++++++++++++++----------------- widget/src/text_editor.rs | 11 +- 4 files changed, 295 insertions(+), 258 deletions(-) diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index 71bd1682..3ec6de6a 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -190,6 +190,8 @@ impl text::Editor for () { fn perform(&mut self, _action: text::editor::Action) {} + fn move_to(&mut self, _cursor: text::editor::Cursor) {} + fn bounds(&self) -> Size { Size::ZERO } diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs index 7b31091b..e0b87756 100644 --- a/core/src/text/editor.rs +++ b/core/src/text/editor.rs @@ -35,6 +35,9 @@ pub trait Editor: Sized + Default { /// Performs an [`Action`] on the [`Editor`]. fn perform(&mut self, action: Action); + /// Moves the cursor to the given position. + fn move_to(&mut self, cursor: Cursor); + /// Returns the current boundaries of the [`Editor`]. fn bounds(&self) -> Size; diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 0114d504..416158d3 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -56,6 +56,31 @@ impl Editor { .as_ref() .expect("Editor should always be initialized") } + + fn with_internal_mut( + &mut self, + f: impl FnOnce(&mut Internal) -> T, + ) -> T { + let editor = + self.0.take().expect("Editor should always be initialized"); + + // TODO: Handle multiple strong references somehow + let mut internal = Arc::try_unwrap(editor) + .expect("Editor cannot have multiple strong references"); + + // Clear cursor cache + let _ = internal + .selection + .write() + .expect("Write to cursor cache") + .take(); + + let result = f(&mut internal); + + self.0 = Some(Arc::new(internal)); + + result + } } impl editor::Editor for Editor { @@ -281,213 +306,228 @@ impl editor::Editor for Editor { let mut font_system = text::font_system().write().expect("Write font system"); - let editor = - self.0.take().expect("Editor should always be initialized"); + self.with_internal_mut(|internal| { + let editor = &mut internal.editor; - // TODO: Handle multiple strong references somehow - let mut internal = Arc::try_unwrap(editor) - .expect("Editor cannot have multiple strong references"); + match action { + // Motion events + Action::Move(motion) => { + if let Some((start, end)) = editor.selection_bounds() { + editor.set_selection(cosmic_text::Selection::None); - let editor = &mut internal.editor; - - // Clear cursor cache - let _ = internal - .selection - .write() - .expect("Write to cursor cache") - .take(); - - match action { - // Motion events - Action::Move(motion) => { - if let Some((start, end)) = editor.selection_bounds() { - editor.set_selection(cosmic_text::Selection::None); - - match motion { - // These motions are performed as-is even when a selection - // is present - Motion::Home - | Motion::End - | Motion::DocumentStart - | Motion::DocumentEnd => { - editor.action( - font_system.raw(), - cosmic_text::Action::Motion(to_motion(motion)), - ); + match motion { + // These motions are performed as-is even when a selection + // is present + Motion::Home + | Motion::End + | Motion::DocumentStart + | Motion::DocumentEnd => { + editor.action( + font_system.raw(), + cosmic_text::Action::Motion(to_motion( + motion, + )), + ); + } + // Other motions simply move the cursor to one end of the selection + _ => editor.set_cursor(match motion.direction() { + Direction::Left => start, + Direction::Right => end, + }), } - // Other motions simply move the cursor to one end of the selection - _ => editor.set_cursor(match motion.direction() { - Direction::Left => start, - Direction::Right => end, - }), + } else { + editor.action( + font_system.raw(), + cosmic_text::Action::Motion(to_motion(motion)), + ); } - } else { + } + + // Selection events + Action::Select(motion) => { + let cursor = editor.cursor(); + + if editor.selection_bounds().is_none() { + editor.set_selection(cosmic_text::Selection::Normal( + cursor, + )); + } + editor.action( font_system.raw(), cosmic_text::Action::Motion(to_motion(motion)), ); + + // Deselect if selection matches cursor position + if let Some((start, end)) = editor.selection_bounds() + && start.line == end.line + && start.index == end.index + { + editor.set_selection(cosmic_text::Selection::None); + } } - } - - // Selection events - Action::Select(motion) => { - let cursor = editor.cursor(); - - if editor.selection_bounds().is_none() { - editor - .set_selection(cosmic_text::Selection::Normal(cursor)); - } - - editor.action( - font_system.raw(), - cosmic_text::Action::Motion(to_motion(motion)), - ); - - // Deselect if selection matches cursor position - if let Some((start, end)) = editor.selection_bounds() - && start.line == end.line - && start.index == end.index - { - editor.set_selection(cosmic_text::Selection::None); - } - } - Action::SelectWord => { - let cursor = editor.cursor(); - - editor.set_selection(cosmic_text::Selection::Word(cursor)); - } - Action::SelectLine => { - let cursor = editor.cursor(); - - editor.set_selection(cosmic_text::Selection::Line(cursor)); - } - Action::SelectAll => { - let buffer = buffer_from_editor(editor); - - if buffer.lines.len() > 1 - || buffer - .lines - .first() - .is_some_and(|line| !line.text().is_empty()) - { + Action::SelectWord => { let cursor = editor.cursor(); - editor.set_selection(cosmic_text::Selection::Normal( - cosmic_text::Cursor { - line: 0, - index: 0, - ..cursor - }, - )); + editor.set_selection(cosmic_text::Selection::Word(cursor)); + } + Action::SelectLine => { + let cursor = editor.cursor(); + editor.set_selection(cosmic_text::Selection::Line(cursor)); + } + Action::SelectAll => { + let buffer = buffer_from_editor(editor); + + if buffer.lines.len() > 1 + || buffer + .lines + .first() + .is_some_and(|line| !line.text().is_empty()) + { + let cursor = editor.cursor(); + + editor.set_selection(cosmic_text::Selection::Normal( + cosmic_text::Cursor { + line: 0, + index: 0, + ..cursor + }, + )); + + editor.action( + font_system.raw(), + cosmic_text::Action::Motion( + cosmic_text::Motion::BufferEnd, + ), + ); + } + } + + // Editing events + Action::Edit(edit) => { + let topmost_line_before_edit = editor + .selection_bounds() + .map(|(start, _)| start) + .unwrap_or_else(|| editor.cursor()) + .line; + + match edit { + Edit::Insert(c) => { + editor.action( + font_system.raw(), + cosmic_text::Action::Insert(c), + ); + } + Edit::Paste(text) => { + editor.insert_string(&text, None); + } + Edit::Indent => { + editor.action( + font_system.raw(), + cosmic_text::Action::Indent, + ); + } + Edit::Unindent => { + editor.action( + font_system.raw(), + cosmic_text::Action::Unindent, + ); + } + Edit::Enter => { + editor.action( + font_system.raw(), + cosmic_text::Action::Enter, + ); + } + Edit::Backspace => { + editor.action( + font_system.raw(), + cosmic_text::Action::Backspace, + ); + } + Edit::Delete => { + editor.action( + font_system.raw(), + cosmic_text::Action::Delete, + ); + } + } + + let cursor = editor.cursor(); + let selection_start = editor + .selection_bounds() + .map(|(start, _)| start) + .unwrap_or(cursor); + + internal.topmost_line_changed = Some( + selection_start.line.min(topmost_line_before_edit), + ); + } + + // Mouse events + Action::Click(position) => { editor.action( font_system.raw(), - cosmic_text::Action::Motion( - cosmic_text::Motion::BufferEnd, - ), + cosmic_text::Action::Click { + x: position.x as i32, + y: position.y as i32, + }, + ); + } + Action::Drag(position) => { + editor.action( + font_system.raw(), + cosmic_text::Action::Drag { + x: position.x as i32, + y: position.y as i32, + }, + ); + + // Deselect if selection matches cursor position + if let Some((start, end)) = editor.selection_bounds() + && start.line == end.line + && start.index == end.index + { + editor.set_selection(cosmic_text::Selection::None); + } + } + Action::Scroll { lines } => { + editor.action( + font_system.raw(), + cosmic_text::Action::Scroll { + pixels: lines as f32 + * buffer_from_editor(editor) + .metrics() + .line_height, + }, ); } } + }); + } - // Editing events - Action::Edit(edit) => { - let topmost_line_before_edit = editor - .selection_bounds() - .map(|(start, _)| start) - .unwrap_or_else(|| editor.cursor()) - .line; + fn move_to(&mut self, cursor: Cursor) { + self.with_internal_mut(|internal| { + // TODO: Expose `Affinity` + internal.editor.set_cursor(cosmic_text::Cursor { + line: cursor.position.line, + index: cursor.position.column, + affinity: cosmic_text::Affinity::Before, + }); - match edit { - Edit::Insert(c) => { - editor.action( - font_system.raw(), - cosmic_text::Action::Insert(c), - ); - } - Edit::Paste(text) => { - editor.insert_string(&text, None); - } - Edit::Indent => { - editor.action( - font_system.raw(), - cosmic_text::Action::Indent, - ); - } - Edit::Unindent => { - editor.action( - font_system.raw(), - cosmic_text::Action::Unindent, - ); - } - Edit::Enter => { - editor.action( - font_system.raw(), - cosmic_text::Action::Enter, - ); - } - Edit::Backspace => { - editor.action( - font_system.raw(), - cosmic_text::Action::Backspace, - ); - } - Edit::Delete => { - editor.action( - font_system.raw(), - cosmic_text::Action::Delete, - ); - } - } - - let cursor = editor.cursor(); - let selection_start = editor - .selection_bounds() - .map(|(start, _)| start) - .unwrap_or(cursor); - - internal.topmost_line_changed = - Some(selection_start.line.min(topmost_line_before_edit)); + if let Some(selection) = cursor.selection { + internal + .editor + .set_selection(cosmic_text::Selection::Normal( + cosmic_text::Cursor { + line: selection.line, + index: selection.column, + affinity: cosmic_text::Affinity::Before, + }, + )); } - - // Mouse events - Action::Click(position) => { - editor.action( - font_system.raw(), - cosmic_text::Action::Click { - x: position.x as i32, - y: position.y as i32, - }, - ); - } - Action::Drag(position) => { - editor.action( - font_system.raw(), - cosmic_text::Action::Drag { - x: position.x as i32, - y: position.y as i32, - }, - ); - - // Deselect if selection matches cursor position - if let Some((start, end)) = editor.selection_bounds() - && start.line == end.line - && start.index == end.index - { - editor.set_selection(cosmic_text::Selection::None); - } - } - Action::Scroll { lines } => { - editor.action( - font_system.raw(), - cosmic_text::Action::Scroll { - pixels: lines as f32 - * buffer_from_editor(editor).metrics().line_height, - }, - ); - } - } - - self.0 = Some(Arc::new(internal)); + }); } fn bounds(&self) -> Size { @@ -512,94 +552,83 @@ impl editor::Editor for Editor { new_wrapping: Wrapping, new_highlighter: &mut impl Highlighter, ) { - let editor = - self.0.take().expect("Editor should always be initialized"); + self.with_internal_mut(|internal| { + let mut font_system = + text::font_system().write().expect("Write font system"); - let mut internal = Arc::try_unwrap(editor) - .expect("Editor cannot have multiple strong references"); + let buffer = buffer_mut_from_editor(&mut internal.editor); - let mut font_system = - text::font_system().write().expect("Write font system"); + if font_system.version() != internal.version { + log::trace!("Updating `FontSystem` of `Editor`..."); - let buffer = buffer_mut_from_editor(&mut internal.editor); + for line in buffer.lines.iter_mut() { + line.reset(); + } - if font_system.version() != internal.version { - log::trace!("Updating `FontSystem` of `Editor`..."); - - for line in buffer.lines.iter_mut() { - line.reset(); + internal.version = font_system.version(); + internal.topmost_line_changed = Some(0); } - internal.version = font_system.version(); - internal.topmost_line_changed = Some(0); - } + if new_font != internal.font { + log::trace!("Updating font of `Editor`..."); - if new_font != internal.font { - log::trace!("Updating font of `Editor`..."); + for line in buffer.lines.iter_mut() { + let _ = line.set_attrs_list(cosmic_text::AttrsList::new( + &text::to_attributes(new_font), + )); + } - for line in buffer.lines.iter_mut() { - let _ = line.set_attrs_list(cosmic_text::AttrsList::new( - &text::to_attributes(new_font), - )); + internal.font = new_font; + internal.topmost_line_changed = Some(0); } - internal.font = new_font; - internal.topmost_line_changed = Some(0); - } + let metrics = buffer.metrics(); + let new_line_height = new_line_height.to_absolute(new_size); - let metrics = buffer.metrics(); - let new_line_height = new_line_height.to_absolute(new_size); + if new_size.0 != metrics.font_size + || new_line_height.0 != metrics.line_height + { + log::trace!("Updating `Metrics` of `Editor`..."); - if new_size.0 != metrics.font_size - || new_line_height.0 != metrics.line_height - { - log::trace!("Updating `Metrics` of `Editor`..."); + buffer.set_metrics( + font_system.raw(), + cosmic_text::Metrics::new(new_size.0, new_line_height.0), + ); + } - buffer.set_metrics( - font_system.raw(), - cosmic_text::Metrics::new(new_size.0, new_line_height.0), - ); - } + let new_wrap = text::to_wrap(new_wrapping); - let new_wrap = text::to_wrap(new_wrapping); + if new_wrap != buffer.wrap() { + log::trace!("Updating `Wrap` strategy of `Editor`..."); - if new_wrap != buffer.wrap() { - log::trace!("Updating `Wrap` strategy of `Editor`..."); + buffer.set_wrap(font_system.raw(), new_wrap); + } - buffer.set_wrap(font_system.raw(), new_wrap); - } + if new_bounds != internal.bounds { + log::trace!("Updating size of `Editor`..."); - if new_bounds != internal.bounds { - log::trace!("Updating size of `Editor`..."); + buffer.set_size( + font_system.raw(), + Some(new_bounds.width), + Some(new_bounds.height), + ); - buffer.set_size( - font_system.raw(), - Some(new_bounds.width), - Some(new_bounds.height), - ); + internal.bounds = new_bounds; + } - internal.bounds = new_bounds; - } + if let Some(topmost_line_changed) = + internal.topmost_line_changed.take() + { + log::trace!( + "Notifying highlighter of line \ + change: {topmost_line_changed}" + ); - if let Some(topmost_line_changed) = internal.topmost_line_changed.take() - { - log::trace!( - "Notifying highlighter of line change: {topmost_line_changed}" - ); + new_highlighter.change_line(topmost_line_changed); + } - new_highlighter.change_line(topmost_line_changed); - } - - internal.editor.shape_as_needed(font_system.raw(), false); - - // Clear cursor cache - let _ = internal - .selection - .write() - .expect("Write to cursor cache") - .take(); - - self.0 = Some(Arc::new(internal)); + internal.editor.shape_as_needed(font_system.raw(), false); + }); } fn highlight( diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 751603df..d7b287a1 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -390,7 +390,6 @@ where R: text::Renderer, { editor: R::Editor, - is_dirty: bool, } impl Content @@ -406,7 +405,6 @@ where pub fn with_text(text: &str) -> Self { Self(RefCell::new(Internal { editor: R::Editor::with_text(text), - is_dirty: true, })) } @@ -415,7 +413,13 @@ where let internal = self.0.get_mut(); internal.editor.perform(action); - internal.is_dirty = true; + } + + /// Moves the current cursor to reflect the given one. + pub fn move_to(&mut self, cursor: Cursor) { + let internal = self.0.get_mut(); + + internal.editor.move_to(cursor); } /// Returns the current cursor position of the [`Content`]. @@ -511,7 +515,6 @@ where f.debug_struct("Content") .field("editor", &internal.editor) - .field("is_dirty", &internal.is_dirty) .finish() } } From fae0138e5e7764e74541c36516acbd2fedab3872 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 1 Dec 2025 20:17:08 +0100 Subject: [PATCH 3/3] Export `Cursor` in `widget::text_editor` --- widget/src/text_editor.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index d7b287a1..0672bee0 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -39,7 +39,7 @@ use crate::core::keyboard::key; use crate::core::layout::{self, Layout}; use crate::core::mouse; use crate::core::renderer; -use crate::core::text::editor::{Cursor, Editor as _}; +use crate::core::text::editor::Editor as _; use crate::core::text::highlighter::{self, Highlighter}; use crate::core::text::{self, LineHeight, Text, Wrapping}; use crate::core::theme; @@ -60,7 +60,7 @@ use std::ops::DerefMut; use std::sync::Arc; pub use text::editor::{ - Action, Edit, Line, LineEnding, Motion, Position, Selection, + Action, Cursor, Edit, Line, LineEnding, Motion, Position, Selection, }; /// A multi-line text input.