From a7ca336eb4db0fe89d2bdeb90b57140e0ebca141 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Tue, 19 May 2026 19:36:22 -0600 Subject: [PATCH] feat: HasSelectableText impl to TextEditor --- core/src/text/editor.rs | 2 + graphics/src/text/editor.rs | 4 ++ widget/src/text_editor.rs | 126 +++++++++++++++++++++++++++++++++++- 3 files changed, 131 insertions(+), 1 deletion(-) diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs index e0b87756..ff89af01 100644 --- a/core/src/text/editor.rs +++ b/core/src/text/editor.rs @@ -78,6 +78,8 @@ pub enum Action { SelectLine, /// Select the entire buffer. SelectAll, + /// Clear the selection + ClearSelection, /// Perform an [`Edit`]. Edit(Edit), /// Click the [`Editor`] at the given [`Point`]. diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index b284c76b..1586479e 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -336,6 +336,10 @@ impl editor::Editor for Editor { } } + Action::ClearSelection => { + editor.set_selection(cosmic_text::Selection::None); + } + // Editing events Action::Edit(edit) => { let topmost_line_before_edit = editor diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index b93c96f3..a8f243fa 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -544,6 +544,8 @@ pub struct State { highlighter: RefCell, highlighter_settings: Highlighter::Settings, highlighter_format_address: usize, + context_menu_position: Option, + pending_edit: Option, } #[derive(Debug, Clone)] @@ -579,6 +581,14 @@ impl State { pub fn is_focused(&self) -> bool { self.focus.is_some() } + + /// Clears focus, selection, and all interaction state. + pub fn clear_focus(&mut self) { + self.focus = None; + self.drag_click = None; + self.context_menu_position = None; + self.pending_edit = Some(Action::ClearSelection); + } } impl operation::Focusable @@ -593,7 +603,7 @@ impl operation::Focusable } fn unfocus(&mut self) { - self.focus = None; + self.clear_focus(); } } @@ -621,6 +631,8 @@ where )), highlighter_settings: self.highlighter_settings.clone(), highlighter_format_address: self.highlighter_format as usize, + context_menu_position: None, + pending_edit: None, }) } @@ -699,6 +711,58 @@ where shell: &mut Shell<'_, Message>, _viewport: &Rectangle, ) { + // Handle context menu and focus-loss on clicks outside. + { + let state = tree.state.downcast_mut::>(); + let text_bounds = layout.bounds(); + + if matches!( + event, + Event::Mouse(mouse::Event::ButtonPressed(_)) + | Event::Touch( + crate::core::touch::Event::FingerPressed { .. } + ) + ) && cursor.position_over(text_bounds).is_none() + { + state.clear_focus(); + } + + match event { + Event::Mouse(mouse::Event::ButtonPressed( + mouse::Button::Right, + )) => { + if let Some(pos) = cursor.position_over(text_bounds) { + if state.focus.is_none() { + state.focus = Some(Focus::now()); + } + state.context_menu_position = Some(pos); + shell.capture_event(); + return; + } + } + Event::Mouse(mouse::Event::ButtonPressed( + mouse::Button::Left, + )) => { + if state.context_menu_position.take().is_some() { + shell.capture_event(); + return; + } + } + _ => {} + } + } + + // Process pending edits from context menu + // publishes via on_edit so the app calls content.perform(). + { + let state = tree.state.downcast_mut::>(); + if let Some(action) = state.pending_edit.take() { + if let Some(on_edit) = self.on_edit.as_ref() { + shell.publish((on_edit)(action)); + } + } + } + let Some(on_edit) = self.on_edit.as_ref() else { return; }; @@ -1545,3 +1609,63 @@ pub(crate) fn convert_macos_shortcut( Some(keyboard::Key::Named(key)) } + +use crate::core::widget::text::HasSelectableText; + +impl HasSelectableText + for TextEditor<'_, Highlighter, Message, Theme, Renderer> +where + Highlighter: text::Highlighter, + Theme: Catalog, + Renderer: text::Renderer, +{ + fn selected_text(&self, _tree: &widget::Tree) -> Option { + self.content.selection() + } + + fn select_all(&self, tree: &mut widget::Tree) { + let state = tree.state.downcast_mut::>(); + state.pending_edit = Some(Action::SelectAll); + } + + fn is_editable(&self) -> bool { + self.on_edit.is_some() + } + + fn is_focused(&self, tree: &widget::Tree) -> bool { + tree.state.downcast_ref::>().is_focused() + } + + fn context_menu_position(&self, tree: &widget::Tree) -> Option { + tree.state + .downcast_ref::>() + .context_menu_position + } + + fn set_context_menu_position( + &self, + tree: &mut widget::Tree, + pos: Option, + ) { + tree.state + .downcast_mut::>() + .context_menu_position = pos; + } + + fn delete_selection(&self, tree: &mut widget::Tree) -> Option { + let state = tree.state.downcast_mut::>(); + state.pending_edit = Some(Action::Edit(Edit::Delete)); + Some(String::new()) + } + + fn paste_text( + &self, + tree: &mut widget::Tree, + text: &str, + ) -> Option { + let state = tree.state.downcast_mut::>(); + state.pending_edit = + Some(Action::Edit(Edit::Paste(Arc::new(text.to_owned())))); + Some(String::new()) + } +}