From 5c61312d81157e9092fa8aadebf8fa7122d57713 Mon Sep 17 00:00:00 2001 From: RiceDee <345114586+RiceDee@users.noreply.github.com> Date: Fri, 23 Oct 2026 15:05:23 +1100 Subject: [PATCH] fix(ui): Redo after Undo in a page text box puts the typing back With typing undone in a paragraph or added text box, its text matches the page again, so Undo and Redo went to the document. Redo then closed the box and redid a document change (or nothing), instead of putting the typing back as the box's own Redo did before. And Shift+Cmd+Z is also Cmd+Z to the Undo shortcut, so even a Redo the box kept was taken as a document Undo. Redo stays with the box while its own history has typing to put back (the test egui makes when it handles the key), and Shift+Cmd+Z is never a document Undo. --- crates/ui-egui/src/commands.rs | 10 ++++++++-- crates/ui-egui/src/editing.rs | 21 ++++++++++++++++++++ crates/ui-egui/tests/it/content.rs | 30 +++++++++++++++++++++++++++++ crates/ui-egui/tests/it/editing.rs | 31 ++++++++++++++++++++++++++++++ 4 files changed, 90 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/commands.rs b/crates/ui-egui/src/commands.rs index ee19c168eb..fb4d6f1b6f 100644 --- a/crates/ui-egui/src/commands.rs +++ b/crates/ui-egui/src/commands.rs @@ -727,8 +727,14 @@ impl PdfCraftApp { continue; } // A box typed into on the page (a paragraph being edited, added text) keeps Undo and - // Redo only while it holds typing the page doesn't have; then they reach the document. - let page_box = matches!(spec.id, "edit.undo" | "edit.redo") && self.page_box_focused(ctx) && !self.page_typing_unapplied(); + // Redo only while it holds typing the page doesn't have, or Redo has typing to put + // back; then they reach the document. ⌘⇧Z is also ⌘Z to the Undo shortcut, so a + // Redo the box keeps is never taken as Undo. + let page_box = matches!(spec.id, "edit.undo" | "edit.redo") + && self.page_box_focused(ctx) + && !self.page_typing_unapplied() + && !(spec.id == "edit.redo" && self.page_box_can_redo(ctx)) + && !(spec.id == "edit.undo" && ctx.input(|i| i.modifiers.shift)); if typing && !spec.in_text && !page_box { continue; } diff --git a/crates/ui-egui/src/editing.rs b/crates/ui-egui/src/editing.rs index 6f16837242..2b31091084 100644 --- a/crates/ui-egui/src/editing.rs +++ b/crates/ui-egui/src/editing.rs @@ -293,6 +293,27 @@ impl PdfCraftApp { focused == Some(crate::edit_text_ui::input_id()) || focused == Some(crate::content_ui::input_id()) } + /// Whether the focused box on the page has typing its own Redo would put back: after Undo in + /// the box restores what the page has, Redo still belongs to the box, not the document. The + /// same test egui makes when it handles the key. + pub(crate) fn page_box_can_redo(&self, ctx: &egui::Context) -> bool { + let Some(view) = self.active.and_then(|i| self.views.get(i)) else { return false }; + let focused = ctx.memory(|m| m.focused()); + let (id, text) = if focused == Some(crate::edit_text_ui::input_id()) + && let Some(ed) = &view.line_editor + { + (crate::edit_text_ui::input_id(), &ed.text) + } else if focused == Some(crate::content_ui::input_id()) + && let Some(d) = &view.content.draft + { + (crate::content_ui::input_id(), &d.text) + } else { + return false; + }; + egui::TextEdit::load_state(ctx, id) + .is_some_and(|state| state.cursor.char_range().is_some_and(|range| state.undoer().has_redo(&(range, text.clone())))) + } + /// Typing in a box on the page (a paragraph being edited, added text) that the page doesn't /// have yet: Undo and Redo keys take it back in the box first. pub(crate) fn page_typing_unapplied(&self) -> bool { diff --git a/crates/ui-egui/tests/it/content.rs b/crates/ui-egui/tests/it/content.rs index 0476064618..7edb13ef6a 100644 --- a/crates/ui-egui/tests/it/content.rs +++ b/crates/ui-egui/tests/it/content.rs @@ -915,3 +915,33 @@ fn aligning_a_selected_line_of_added_text_aligns_only_that_line() { assert_eq!(aligns(&h), (pdfcraft_engine::TextAlign::Left, vec![]), "Undo puts it back"); assert!(h.state().views[0].content.draft.is_none(), "and closes the box"); } + +/// The same in an added text box: Redo after Undo puts the typing back in the box, and the +/// document's own Redo stays waiting. +#[test] +fn redo_after_undoing_typing_in_added_text_puts_it_back() { + let mut h = harness(); + let rect = [40.0, 195.0, 240.0, 210.0]; + let text = pdfcraft_engine::AddedText { rect, text: "Reviewed".into(), ..Default::default() }; + assert!(h.state_mut().apply_edit(pdfcraft_engine::Edit::AddText { page: 0, text: text.clone() })); + let bold = pdfcraft_engine::AddedText { bold: true, ..text }; + assert!(h.state_mut().apply_edit(pdfcraft_engine::Edit::UpdateContent { page: 0, index: 0, content: pdfcraft_engine::AddedContent::Text(bold) })); + assert!(h.state_mut().execute("edit.undo")); + assert!(h.state_mut().execute("edit.text")); + h.run_steps(3); + let point = at(&h, 45.0, 195.0); + click(&mut h, point); + assert_eq!(h.state().views[0].content.draft.as_ref().and_then(|d| d.index), Some(0), "the editor opened on the text"); + h.key_press_modifiers(egui::Modifiers::COMMAND, egui::Key::End); + h.event(egui::Event::Text(" twice".into())); + h.run_steps(2); + h.key_press_modifiers(egui::Modifiers::COMMAND, egui::Key::Z); + h.run_steps(3); + assert_eq!(h.state().views[0].content.draft.as_ref().map(|d| d.text.as_str()), Some("Reviewed")); + h.key_press_modifiers(egui::Modifiers::COMMAND | egui::Modifiers::SHIFT, egui::Key::Z); + h.run_steps(3); + assert_eq!(h.state().views[0].content.draft.as_ref().map(|d| d.text.as_str()), Some("Reviewed twice"), "Redo puts the typing back"); + let s = h.state(); + let doc = s.session.get(s.views[0].id).unwrap(); + assert_eq!(doc.can_redo(), Some("Edit content"), "the document's Redo still waits"); +} diff --git a/crates/ui-egui/tests/it/editing.rs b/crates/ui-egui/tests/it/editing.rs index d51f7f551f..b19f69cb46 100644 --- a/crates/ui-egui/tests/it/editing.rs +++ b/crates/ui-egui/tests/it/editing.rs @@ -6127,3 +6127,34 @@ fn aligning_a_selected_row_aligns_only_that_row_and_undo_puts_it_back() { assert!(s.views[0].line_editor.is_none(), "the editor closed with the step it showed"); assert!((row_rect(&h, "nine")[0] - last[0]).abs() < 0.5, "the last row is back"); } + +/// Redo after Undo in the box puts the typing back, in the box: it doesn't reach the document, +/// whose own Redo (a formatting change waiting here) must stay waiting. +#[test] +fn redo_after_undoing_typing_puts_the_typing_back() { + let mut h = harness(1, |_| {}); + assert!(h.state_mut().execute("edit.edit_text")); + h.run_steps(2); + open_paragraph(&mut h); + h.get_by_label("Bold").click(); + h.run_steps(4); + // Undo the bold (closing the editor), so the document has it to redo. + h.key_press_modifiers(Modifiers::COMMAND, Key::Z); + h.run_steps(4); + assert_eq!(h.state().session.get(h.state().views[0].id).unwrap().can_redo(), Some("Edit text")); + open_paragraph(&mut h); + h.key_press_modifiers(Modifiers::COMMAND, Key::End); + h.event(egui::Event::Text(" more".into())); + h.run_steps(2); + let typed = h.state().views[0].line_editor.as_ref().unwrap().text.clone(); + h.key_press_modifiers(Modifiers::COMMAND, Key::Z); + h.run_steps(3); + assert_ne!(h.state().views[0].line_editor.as_ref().unwrap().text, typed, "the typing is taken back"); + h.key_press_modifiers(Modifiers::COMMAND | Modifiers::SHIFT, Key::Z); + h.run_steps(3); + let s = h.state(); + let ed = s.views[0].line_editor.as_ref().expect("still typing"); + assert_eq!(ed.text, typed, "Redo puts the typing back"); + let doc = s.session.get(s.views[0].id).unwrap(); + assert_eq!((doc.can_redo(), doc.text_blocks(0)[0].base_font.as_str()), (Some("Edit text"), "Helvetica"), "the document's Redo still waits"); +}