diff --git a/crates/ui-egui/src/edit_text_ui.rs b/crates/ui-egui/src/edit_text_ui.rs index 396c430a7f..4724cbb6e5 100644 --- a/crates/ui-egui/src/edit_text_ui.rs +++ b/crates/ui-egui/src/edit_text_ui.rs @@ -276,15 +276,33 @@ pub(crate) fn input_id() -> egui::Id { /// as it was before a format change, into a box showing the page as it is now. `from` is the box's /// own record of that; which document `doc` last used the history is kept beside it, since a box /// in another tab on the same text would otherwise take that document's typing with its Redo. +/// When another document's box takes the text box, the box it leaves keeps its history (and +/// caret) aside, and gets it back on its return, so its typing can still be undone there. pub(crate) fn forget_typing_before(ctx: &egui::Context, id: egui::Id, doc: pdfcraft_engine::DocId, from: &mut Option, text: &str) { let owner = id.with("typing-doc"); - if from.as_deref() == Some(text) && ctx.data(|d| d.get_temp::(owner)) == Some(doc) { + let last = ctx.data(|d| d.get_temp::(owner)); + if from.as_deref() == Some(text) && last == Some(doc) { return; } - if let Some(mut state) = egui::TextEdit::load_state(ctx, id) { - state.clear_undoer(); - egui::TextEdit::store_state(ctx, id, state); + let aside = |doc: pdfcraft_engine::DocId| id.with(("typing-aside", doc)); + if let Some(left) = last.filter(|l| *l != doc) + && let Some(state) = egui::TextEdit::load_state(ctx, id) + { + // A copy of its own: a state's history is shared between its clones, and the next box + // clears it in place. + let mut kept = egui::text_edit::TextEditState::default(); + kept.cursor = state.cursor; + kept.set_undoer(state.undoer()); + ctx.data_mut(|d| d.insert_temp(aside(left), kept)); } + // Only the same box, still on the text it opened on, takes its history back. + let back = ctx.data_mut(|d| d.remove_temp::(aside(doc))).filter(|_| from.as_deref() == Some(text)); + let state = back.unwrap_or_else(|| { + let mut state = egui::TextEdit::load_state(ctx, id).unwrap_or_default(); + state.clear_undoer(); + state + }); + egui::TextEdit::store_state(ctx, id, state); ctx.data_mut(|d| d.insert_temp(owner, doc)); *from = Some(text.to_owned()); } diff --git a/crates/ui-egui/tests/it/content.rs b/crates/ui-egui/tests/it/content.rs index a17d0793ea..43771ed724 100644 --- a/crates/ui-egui/tests/it/content.rs +++ b/crates/ui-egui/tests/it/content.rs @@ -1022,6 +1022,42 @@ fn redo_from_the_menu_is_enabled_for_the_added_text_boxs_own_typing() { assert_eq!(h.state().views[0].content.draft.as_ref().map(|d| d.text.as_str()), Some("Reviewed twice"), "Redo puts it back"); } +/// Typing in tab A's added text box, the same text opened in tab B, then back to A: Edit ▸ Undo +/// takes the typing back in A's box, which stays open. +#[test] +fn undo_after_a_round_trip_through_another_tab_takes_back_added_text_typing() { + let mut h = harness(); + h.state_mut().open_bytes("copy.pdf", None, include_bytes!("../data/form.pdf").to_vec()).unwrap(); + h.run_steps(4); + let text = pdfcraft_engine::AddedText { rect: [40.0, 195.0, 240.0, 210.0], text: "Reviewed".into(), ..Default::default() }; + let open_in = |h: &mut Harness<'static, PdfCraftApp>, tab: usize| { + h.state_mut().active = Some(tab); + h.run_steps(2); + assert!(h.state_mut().execute("edit.text")); + h.run_steps(3); + let r = h.state().views[tab].page_screen_rect(0).expect("on screen"); + let k = r.width() / 300.0; + click(h, r.min + egui::vec2(45.0 * k, 195.0 * k)); + assert_eq!(h.state().views[tab].content.draft.as_ref().and_then(|d| d.index), Some(0), "the editor opened on the text"); + }; + for tab in [0, 1] { + h.state_mut().active = Some(tab); + assert!(h.state_mut().apply_edit(pdfcraft_engine::Edit::AddText { page: 0, text: text.clone() })); + } + open_in(&mut h, 0); + h.key_press_modifiers(egui::Modifiers::COMMAND, egui::Key::End); + h.event(egui::Event::Text(" twice".into())); + h.run_steps(2); + open_in(&mut h, 1); + h.state_mut().active = Some(0); + h.run_steps(3); + let draft = |h: &Harness<'static, PdfCraftApp>| h.state().views[0].content.draft.as_ref().map(|d| d.text.clone()); + assert_eq!(draft(&h).as_deref(), Some("Reviewed twice"), "A's box is still open with the typing"); + assert!(h.state_mut().execute("edit.undo")); + h.run_steps(3); + assert_eq!(draft(&h).as_deref(), Some("Reviewed"), "Undo takes the typing back in A's box"); +} + /// Redo with an added text box open and unchanged reaches the document's waiting Redo, even when /// the box's text ends in a space: closing the unchanged box writes nothing, so it can't clear /// that Redo first. diff --git a/crates/ui-egui/tests/it/editing.rs b/crates/ui-egui/tests/it/editing.rs index c22ce087f2..c1acc43fe7 100644 --- a/crates/ui-egui/tests/it/editing.rs +++ b/crates/ui-egui/tests/it/editing.rs @@ -6645,6 +6645,49 @@ fn redo_from_the_menu_is_enabled_for_the_boxs_own_typing() { assert_eq!(h.state().views[0].line_editor.as_ref().expect("still open").text, typed, "Redo puts the typing back"); } +/// Typing in tab A's paragraph, a paragraph opened in tab B, then back to A: Edit ▸ Undo takes +/// the typing back in A's box. The tabs share one text box, and its history was cleared each time +/// another tab's box took it, so Undo found no step and closed A's paragraph instead +/// (skylartaylor's and FredApps's review of 08ec1db7). +#[test] +fn undo_after_a_round_trip_through_another_tab_takes_back_the_typing() { + let mut h = harness(1, |app| { + app.open_bytes("copy.pdf", None, fixture(1)).expect("the copy opens"); + }); + let open_in = |h: &mut Harness<'static, PdfCraftApp>, tab: usize| { + h.state_mut().active = Some(tab); + h.run_steps(2); + if h.state().views[tab].line_editor.is_none() { + assert!(h.state_mut().execute("edit.edit_text")); + h.run_steps(2); + } + let r = h.state().views[tab].page_screen_rect(0).expect("on screen"); + click_at(h, egui::pos2(r.left() + 40.0 / 200.0 * r.width(), r.top() + (300.0 - 158.0) / 300.0 * r.height())); + assert!(h.state().views[tab].line_editor.is_some(), "the paragraph opens for typing"); + }; + open_in(&mut h, 0); + h.get_by_label("Bold").click(); + h.run_steps(4); + h.key_press_modifiers(Modifiers::COMMAND, Key::Z); + h.run_steps(4); + open_in(&mut h, 0); + let original = h.state().views[0].line_editor.as_ref().unwrap().text.clone(); + h.key_press_modifiers(Modifiers::COMMAND, Key::End); + h.event(egui::Event::Text(" more".into())); + h.run_steps(2); + assert!(h.state().views[0].line_editor.as_ref().unwrap().text.ends_with(" more")); + open_in(&mut h, 1); + h.state_mut().active = Some(0); + h.run_steps(3); + assert!(h.state().views[0].line_editor.is_some(), "A's paragraph is still open after the round trip"); + assert!(h.state_mut().execute("edit.undo"), "Undo is enabled"); + h.run_steps(3); + let s = h.state(); + let ed = s.views[0].line_editor.as_ref().expect("the box stays open: Undo takes back the typing"); + assert_eq!(ed.text, original, "the typing is taken back in the box"); + assert_eq!(s.session.get(s.views[0].id).unwrap().can_redo(), Some("Edit text"), "the document's Redo still waits"); +} + /// Two copies of a document, the same paragraph open in each. The paragraph boxes share one text /// box, and so one Undo history: a box opened on the same text as another tab's didn't clear it. /// Typing in one tab, taken back with Undo, then came back with Redo in the other tab.