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] 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), }