diff --git a/crates/ui-egui/src/content_ui.rs b/crates/ui-egui/src/content_ui.rs index d30a814d43..4578d0d9d8 100644 --- a/crates/ui-egui/src/content_ui.rs +++ b/crates/ui-egui/src/content_ui.rs @@ -530,6 +530,7 @@ pub(crate) fn after_refresh(view: &mut DocView, added: &[Added]) { /// committed (a click away, or Done), and whether Done finished adding text. Discard or Escape /// throws the draft away. pub(crate) fn editor(ctx: &egui::Context, view: &mut DocView, info: &DocInfo, added: &[Added]) -> (Option, bool) { + let doc_id = view.id; let Some(page) = view.content.draft.as_ref().map(|d| d.page) else { return (None, false) }; let Some(xf) = view.page_xform(page) else { return (None, false) }; let zoom = xf.rect.width() / xf.pw.max(1.0); @@ -611,7 +612,7 @@ pub(crate) fn editor(ctx: &egui::Context, view: &mut DocView, info: &DocInfo, ad |ui: &egui::Ui, text: &dyn egui::TextBuffer, wrap: f32| aligned_galley(ui, text.as_str(), &style, &line_font, color, wrap); let before = t.text.clone(); let input = input_id(); - crate::edit_text_ui::forget_typing_before(ui.ctx(), input, &mut t.typing_from, &t.style.text); + crate::edit_text_ui::forget_typing_before(ui.ctx(), input, doc_id, &mut t.typing_from, &t.style.text); ui.horizontal_top(|ui| { ui.spacing_mut().item_spacing.x = 4.0; let output = egui::TextEdit::multiline(&mut t.text) diff --git a/crates/ui-egui/src/edit_text_ui.rs b/crates/ui-egui/src/edit_text_ui.rs index 53426d8757..12adb622fb 100644 --- a/crates/ui-egui/src/edit_text_ui.rs +++ b/crates/ui-egui/src/edit_text_ui.rs @@ -266,18 +266,22 @@ pub(crate) fn input_id() -> egui::Id { } /// The page text box `id` keeps its own Undo history, across every paragraph or box it has shown -/// (they share the id). Once the page has `text` (the box opened on it, or formatting applied it), -/// that history is cleared: Undo in the box takes back only what was typed since, and then reaches -/// the document. Kept, it put back text from another paragraph, or the whole paragraph as it was -/// before a format change, into a box showing the page as it is now. -pub(crate) fn forget_typing_before(ctx: &egui::Context, id: egui::Id, from: &mut Option, text: &str) { - if from.as_deref() == Some(text) { +/// in every tab (they share the id). Once the page has `text` (the box opened on it, or formatting +/// applied it), that history is cleared: Undo in the box takes back only what was typed since, and +/// then reaches the document. Kept, it put back text from another paragraph, or the whole paragraph +/// 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. +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) { return; } if let Some(mut state) = egui::TextEdit::load_state(ctx, id) { state.clear_undoer(); egui::TextEdit::store_state(ctx, id, state); } + ctx.data_mut(|d| d.insert_temp(owner, doc)); *from = Some(text.to_owned()); } @@ -1624,6 +1628,7 @@ fn editor_galley(ui: &egui::Ui, ed: &LineEditor, text: &str, scale: f32, width: /// The inline editor; returns the edit once the text is applied. pub(crate) fn overlay(ctx: &egui::Context, view: &mut DocView, info: &DocInfo) -> Option { + let view_id = view.id; // Clicks outside the document (the Format text panel) keep the paragraph open. // Where the click that took the focus was: the pointer itself may be gone by then (a touch, a // pen lifted away). @@ -1743,7 +1748,7 @@ pub(crate) fn overlay(ctx: &egui::Context, view: &mut DocView, info: &DocInfo) - }; let before = ed.text.clone(); let input = input_id(); - forget_typing_before(ui.ctx(), input, &mut ed.typing_from, &ed.original); + forget_typing_before(ui.ctx(), input, view_id, &mut ed.typing_from, &ed.original); let output = egui::TextEdit::multiline(&mut ed.text) .id(input) .font(font.clone()) diff --git a/crates/ui-egui/tests/it/content.rs b/crates/ui-egui/tests/it/content.rs index 1fd887bf5e..18e3144f83 100644 --- a/crates/ui-egui/tests/it/content.rs +++ b/crates/ui-egui/tests/it/content.rs @@ -1033,3 +1033,40 @@ fn redo_with_an_unchanged_box_open_redoes_the_document() { assert!(now.bold, "the waiting Redo was redone"); assert!(h.state().views[0].content.draft.is_none(), "the box closed with the step"); } + +/// Two copies of a document, the same added text open in each: the boxes share one Undo history, +/// so typing taken back with Undo in one tab came back with Redo in the other. +#[test] +fn typing_in_one_tab_never_reaches_another_tabs_added_text() { + 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, 1); + open_in(&mut h, 0); + h.key_press_modifiers(egui::Modifiers::COMMAND, egui::Key::End); + h.event(egui::Event::Text(" secret".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"), "taken back"); + h.state_mut().active = Some(1); + h.run_steps(3); + h.key_press_modifiers(egui::Modifiers::COMMAND | egui::Modifiers::SHIFT, egui::Key::Z); + h.run_steps(3); + assert_eq!(h.state().views[1].content.draft.as_ref().map(|d| d.text.as_str()), Some("Reviewed"), "the other tab's typing stays out"); +} diff --git a/crates/ui-egui/tests/it/editing.rs b/crates/ui-egui/tests/it/editing.rs index 55a77452fc..f68f997a16 100644 --- a/crates/ui-egui/tests/it/editing.rs +++ b/crates/ui-egui/tests/it/editing.rs @@ -6233,3 +6233,48 @@ fn redo_from_the_menu_puts_the_typing_back_in_the_box() { 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"); } + +/// 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 (the key or the menu) in the +/// other tab. +#[test] +fn typing_in_one_tab_never_reaches_another_tabs_paragraph() { + 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); + 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, 1); + let original = h.state().views[1].line_editor.as_ref().unwrap().text.clone(); + open_in(&mut h, 0); + assert_eq!(h.state().views[0].line_editor.as_ref().unwrap().text, original, "the same paragraph"); + h.key_press_modifiers(Modifiers::COMMAND, Key::End); + h.event(egui::Event::Text(" secret".into())); + h.run_steps(2); + h.key_press_modifiers(Modifiers::COMMAND, Key::Z); + h.run_steps(3); + assert_eq!(h.state().views[0].line_editor.as_ref().unwrap().text, original, "taken back"); + // The copy, its paragraph still open: Redo there has nothing of its own to put back. + h.state_mut().active = Some(1); + h.run_steps(3); + h.key_press_modifiers(Modifiers::COMMAND | Modifiers::SHIFT, Key::Z); + h.run_steps(3); + let text = h.state().views[1].line_editor.as_ref().map(|ed| ed.text.clone()); + assert!(!text.as_deref().unwrap_or_default().contains("secret"), "the other tab's typing came in: {text:?}"); + // Nor through what the menu's Redo runs, which reads the box's history itself (the menu item + // is off here, as the copy's document has nothing to redo). + h.state_mut().redo(); + h.run_steps(3); + let s = h.state(); + let text = s.views[1].line_editor.as_ref().map(|ed| ed.text.clone()); + assert!(!text.as_deref().unwrap_or_default().contains("secret"), "the other tab's typing came in from the menu: {text:?}"); + assert_eq!(s.session.get(s.views[1].id).unwrap().text_blocks(0)[0].text, s.session.get(s.views[0].id).unwrap().text_blocks(0)[0].text); +}