mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-03 06:51:50 -06:00
fix(ui): typing in one tab's text box never reaches another tab's
The paragraph box, and the added-text box, each keep one Undo history under one egui id, shared by every tab. A box cleared it when it opened on new text, but a box in another tab on the same text (two copies of a document) kept it: typing taken back with Undo in one tab came back with Redo in the other. The document that last used the history is now kept beside it, and the history is cleared whenever another document's box takes it over. Tests: the paragraph and added-text cases, each with two copies of a document open.
This commit is contained in:
@@ -518,6 +518,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<Edit>, 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);
|
||||
@@ -571,7 +572,7 @@ pub(crate) fn editor(ctx: &egui::Context, view: &mut DocView, info: &DocInfo, ad
|
||||
ui.visuals_mut().text_cursor.stroke.color = crate::edit_text_ui::caret_color_on(Color32::WHITE);
|
||||
let [cr, cg, cb] = t.style.color.map(|v| (v.clamp(0.0, 1.0) * 255.0) as u8);
|
||||
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 resp = ui.add(
|
||||
|
||||
@@ -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<String>, 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<String>, text: &str) {
|
||||
let owner = id.with("typing-doc");
|
||||
if from.as_deref() == Some(text) && ctx.data(|d| d.get_temp::<pdfcraft_engine::DocId>(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());
|
||||
}
|
||||
|
||||
@@ -1577,6 +1581,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<Edit> {
|
||||
let view_id = view.id;
|
||||
// Clicks outside the document (the Format text panel) keep the paragraph open.
|
||||
let outside = ctx.input(|i| i.pointer.latest_pos()).is_some_and(|p| !view.viewport_rect().contains(p));
|
||||
let page = view.line_editor.as_ref()?.page;
|
||||
@@ -1693,7 +1698,7 @@ pub(crate) fn overlay(ctx: &egui::Context, view: &mut DocView, info: &DocInfo) -
|
||||
if snapshot.changes_with(text.as_str()) { galley } else { unseen(galley) }
|
||||
};
|
||||
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())
|
||||
|
||||
@@ -917,3 +917,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");
|
||||
}
|
||||
|
||||
@@ -6067,3 +6067,41 @@ fn redo_after_undoing_typing_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");
|
||||
}
|
||||
|
||||
/// 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.
|
||||
#[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 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: {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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user