diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 0599e80a7..0b296cf7b 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -1,10 +1,7 @@ use floem::{ prelude::*, views::editor::{ - command::{Command, CommandExecuted}, - core::{ - command::EditCommand, cursor::CursorAffinity, editor::EditType, selection::Selection, - }, + core::{cursor::CursorAffinity, editor::EditType, selection::Selection}, text::{default_dark_color, SimpleStyling}, }, }; @@ -23,36 +20,37 @@ fn app_view() -> impl IntoView { .style(|s| s.size_full()) .editor_style(default_dark_color) .editor_style(move |s| s.hide_gutter(hide_gutter_a.get())); + let focus_editor_a = editor_a.editor().clone(); let editor_b = editor_a .shared_editor() .editor_style(default_dark_color) .editor_style(move |s| s.hide_gutter(hide_gutter_b.get())) .style(|s| s.size_full()) - .pre_command(|ev| { - if matches!(ev.cmd, Command::Edit(EditCommand::Undo)) { - println!("Undo command executed on editor B, ignoring!"); - return CommandExecuted::Yes; - } - CommandExecuted::No - }) .update(|_| { // This hooks up to both editors! println!("Editor changed"); }) .placeholder("Some placeholder text"); let doc = editor_a.doc(); + let clear_editor_a = editor_a.editor().clone(); Stack::new(( editor_a, editor_b, Stack::new(( Button::new("Clear").action(move || { - doc.edit_single( + doc.edit_single_from( + &clear_editor_a, Selection::region(0, doc.text().len(), CursorAffinity::Backward), "", EditType::DeleteSelection, ); }), + Button::new("Focus A").action(move || { + if let Some(id) = focus_editor_a.editor_view_id.get_untracked() { + id.request_focus(); + } + }), Button::new("Flip Gutter").action(move || { hide_gutter_a.update(|hide| *hide = !*hide); hide_gutter_b.update(|hide| *hide = !*hide); diff --git a/examples/syntax-editor/src/main.rs b/examples/syntax-editor/src/main.rs index fd4bc5ffb..d6198d1a7 100644 --- a/examples/syntax-editor/src/main.rs +++ b/examples/syntax-editor/src/main.rs @@ -215,12 +215,14 @@ mod tests { .style(|s| s.size_full()); let doc = editor.doc(); + let clear_editor = editor.editor().clone(); Stack::new(( editor, Stack::new(( Button::new("Clear").action(move || { - doc.edit_single( + doc.edit_single_from( + &clear_editor, Selection::region(0, doc.text().len(), CursorAffinity::Backward), "", EditType::DeleteSelection, diff --git a/examples/widget-gallery/src/texteditor.rs b/examples/widget-gallery/src/texteditor.rs index b5e220ce4..3931b944a 100644 --- a/examples/widget-gallery/src/texteditor.rs +++ b/examples/widget-gallery/src/texteditor.rs @@ -2,10 +2,7 @@ use floem::{ action::inspect, prelude::*, views::editor::{ - command::{Command, CommandExecuted}, - core::{ - command::EditCommand, cursor::CursorAffinity, editor::EditType, selection::Selection, - }, + core::{cursor::CursorAffinity, editor::EditType, selection::Selection}, text::{SimpleStyling, default_dark_color}, }, }; @@ -28,26 +25,21 @@ pub fn editor_view() -> impl IntoView { .editor_style(default_dark_color) .editor_style(move |s| s.hide_gutter(!hide_gutter_a.get())) .style(|s| s.size_full()) - .pre_command(|ev| { - if matches!(ev.cmd, Command::Edit(EditCommand::Undo)) { - println!("Undo command executed on editor B, ignoring!"); - return CommandExecuted::Yes; - } - CommandExecuted::No - }) .update(|_| { // This hooks up to both editors! println!("Editor changed"); }) .placeholder("Some placeholder text"); let doc = editor_a.doc(); + let clear_editor_a = editor_a.editor().clone(); Stack::new(( editor_a, editor_b, Stack::new(( Button::new("Clear").action(move || { - doc.edit_single( + doc.edit_single_from( + &clear_editor_a, Selection::region(0, doc.text().len(), CursorAffinity::Backward), "", EditType::DeleteSelection, diff --git a/src/views/editor/text.rs b/src/views/editor/text.rs index ea2d772d5..d3a45c2c5 100644 --- a/src/views/editor/text.rs +++ b/src/views/editor/text.rs @@ -188,24 +188,44 @@ pub trait Document: DocumentPhantom + ::std::any::Any { self.edit(&mut iter, edit_type); } - /// Perform the edit(s) on this document. + /// Perform a single edit while preserving editor-specific semantics for the initiating editor. /// - /// This intentionally does not require an `Editor` as this is primarily intended for use by - /// code that wants to modify the document from 'outside' the usual keybinding/command logic. + /// Use this when a UI action edits the document on behalf of a live editor and should record + /// that editor's cursor state for undo. + fn edit_single_from( + &self, + editor: &Editor, + selection: Selection, + content: &str, + edit_type: EditType, + ) { + let mut iter = std::iter::once((selection, content)); + self.edit_from(editor, &mut iter, edit_type); + } + + /// Perform the edit(s) on this document. /// - /// ```rust,ignore - /// let editor: TextEditor = text_editor(); - /// let doc: Rc = editor.doc(); + /// This intentionally does not require an `Editor`. It is the raw document mutation path for + /// code that wants to modify text without an initiating editor context. /// - /// stack(( - /// editor, - /// button(|| "Append 'Hello'").on_click_stop(move |_| { - /// let text = doc.text(); - /// doc.edit_single(Selection::caret(text.len()), "Hello", EditType::InsertChars); - /// }) - /// )) - /// ``` + /// Because it has no initiating editor context, it can't record that editor's cursor history + /// for undo. If a UI action is editing on behalf of a live editor, prefer + /// [`Document::edit_from`] or [`Document::edit_single_from`] instead. fn edit(&self, iter: &mut dyn Iterator, edit_type: EditType); + + /// Perform the edit(s) on this document using the provided editor as the initiating context. + /// + /// The default implementation falls back to [`Document::edit`], which keeps this additive for + /// custom `Document` implementations that do not yet preserve initiating-editor cursor history + /// for undo. + fn edit_from( + &self, + _editor: &Editor, + iter: &mut dyn Iterator, + edit_type: EditType, + ) { + self.edit(iter, edit_type); + } } pub trait DocumentPhantom { @@ -516,9 +536,29 @@ where self.doc.edit_single(selection, content, edit_type) } + fn edit_single_from( + &self, + editor: &Editor, + selection: Selection, + content: &str, + edit_type: EditType, + ) { + self.doc + .edit_single_from(editor, selection, content, edit_type) + } + fn edit(&self, iter: &mut dyn Iterator, edit_type: EditType) { self.doc.edit(iter, edit_type) } + + fn edit_from( + &self, + editor: &Editor, + iter: &mut dyn Iterator, + edit_type: EditType, + ) { + self.doc.edit_from(editor, iter, edit_type) + } } impl DocumentPhantom for ExtCmdDocument where diff --git a/src/views/editor/text_document.rs b/src/views/editor/text_document.rs index 1a7c0e5f0..ff7f55f8c 100644 --- a/src/views/editor/text_document.rs +++ b/src/views/editor/text_document.rs @@ -148,6 +148,46 @@ impl TextDocument { }); } + fn apply_programmatic_edit( + &self, + editor: Option<&Editor>, + iter: &mut dyn Iterator, + edit_type: EditType, + ) { + let mut cursor = editor.map(|editor| editor.cursor.get_untracked()); + let cursor_before = cursor.as_ref().map(|cursor| cursor.mode.clone()); + + let deltas = self + .buffer + .try_update(|buffer| buffer.edit(iter, edit_type)); + let deltas = deltas.map(|x| [x]); + let deltas = deltas.as_ref().map(|x| x as &[_]).unwrap_or(&[]); + + if deltas.is_empty() { + return; + } + + if let Some(cursor) = cursor.as_mut() { + for delta in deltas.iter().map(|(_, delta, _)| delta) { + cursor.apply_delta(delta); + } + } + + if let (Some(cursor_before), Some(cursor)) = (cursor_before, cursor.as_ref()) { + self.buffer.update(|buffer| { + buffer.set_cursor_before(cursor_before); + buffer.set_cursor_after(cursor.mode.clone()); + }); + } + + self.update_cache_rev(); + self.on_update(editor, deltas); + + if let (Some(editor), Some(cursor)) = (editor, cursor) { + editor.cursor.set(cursor); + } + } + fn placeholder(&self, editor_id: EditorId) -> Option { self.placeholders .with_untracked(|placeholders| placeholders.get(&editor_id).cloned()) @@ -231,14 +271,16 @@ impl Document for TextDocument { } fn edit(&self, iter: &mut dyn Iterator, edit_type: EditType) { - let deltas = self - .buffer - .try_update(|buffer| buffer.edit(iter, edit_type)); - let deltas = deltas.map(|x| [x]); - let deltas = deltas.as_ref().map(|x| x as &[_]).unwrap_or(&[]); + self.apply_programmatic_edit(None, iter, edit_type); + } - self.update_cache_rev(); - self.on_update(None, deltas); + fn edit_from( + &self, + editor: &Editor, + iter: &mut dyn Iterator, + edit_type: EditType, + ) { + self.apply_programmatic_edit(Some(editor), iter, edit_type); } } impl DocumentPhantom for TextDocument {