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() } }