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.
This commit is contained in:
RiceDee
2026-10-24 02:34:48 +11:00
parent 1f29575d34
commit 5c61312d81
4 changed files with 90 additions and 2 deletions
+8 -2
View File
@@ -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;
}
+21
View File
@@ -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 {
+30
View File
@@ -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");
}
+31
View File
@@ -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");
}