diff --git a/crates/ui-egui/src/editing.rs b/crates/ui-egui/src/editing.rs index 1e9c0d6b7b..4f25d6df21 100644 --- a/crates/ui-egui/src/editing.rs +++ b/crates/ui-egui/src/editing.rs @@ -38,6 +38,11 @@ pub enum SaveTarget { As, } +/// Text typed into a new text box that isn't added to the page yet. +fn new_text_typed(v: &crate::DocView) -> bool { + v.content.draft.as_ref().is_some_and(|d| d.index.is_none() && !d.text.trim().is_empty()) +} + impl PdfCraftApp { /// Apply an edit to the active document. Returns `true` on success; failures are shown. pub fn apply_edit(&mut self, edit: Edit) -> bool { @@ -636,6 +641,7 @@ impl PdfCraftApp { || v.content.blocked().is_some() || v.forms.focus.as_ref().is_some_and(|f| crate::forms_ui::draft_edit(f, &doc.form).is_some()) || v.fill_text.as_ref().is_some_and(|t| !t.text.trim().is_empty()) + || new_text_typed(v) || v.line_editor.as_ref().map_or_else(|| false, |ed| ed.has_unsaved_text()) } @@ -1694,6 +1700,56 @@ mod revert_draft_tests { assert!(app.unsaved_flag.load(Ordering::Relaxed)); } + /// Text typed into a new text box is lost on close unless the save prompt appears; an empty + /// box has nothing to lose. + #[test] + fn text_typed_into_a_new_text_box_counts_as_unsaved_work() { + use crate::content_ui::{TextDraft, default_style}; + let mut app = app(); + let draft = |text: &str| TextDraft { page: 0, index: None, rect: [0.0; 4], text: text.into(), style: default_style(), focus: true }; + assert!(!app.has_unsaved_work(0)); + app.views[0].content.draft = Some(draft(" \n")); + assert!(!app.has_unsaved_work(0)); + app.views[0].content.draft = Some(draft("Typed")); + assert!(!app.session.get(app.views[0].id).unwrap().dirty); + assert!(app.has_unsaved_work(0)); + } + + /// Close ▸ Save, and Quit ▸ Save, write the text typed into a new text box before the tab + /// closes. + #[test] + fn saving_on_close_keeps_text_typed_into_a_new_text_box() { + use crate::content_ui::{TextDraft, default_style}; + use pdfcraft_engine::AddedContent; + for quit in [false, true] { + let dir = std::env::temp_dir().join(format!("pdfcraft-ui-new-text-{}-{quit}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("source.pdf").to_string_lossy().into_owned(); + let bytes = include_bytes!("../tests/data/form.pdf").to_vec(); + std::fs::write(&path, &bytes).unwrap(); + let mut app = PdfCraftApp::new(); + app.open_bytes("source.pdf", Some(path.clone()), bytes).unwrap(); + let rect = [72.0, 72.0, 272.0, 92.0]; + app.views[0].content.draft = Some(TextDraft { page: 0, index: None, rect, text: "Typed".into(), style: default_style(), focus: true }); + let ctx = egui::Context::default(); + if quit { + assert!(!app.request_quit(&ctx)); + } else { + app.request_close_tab(0); + } + assert!(app.close_request.is_some()); + app.resolve_close(&ctx, Some(true)); + assert!(app.views.is_empty(), "the tab closes once saved"); + + let mut reopened = PdfCraftApp::new(); + reopened.open_bytes("source.pdf", Some(path.clone()), std::fs::read(&path).unwrap()).unwrap(); + let doc = reopened.session.get(reopened.views[0].id).unwrap(); + assert!(doc.added().iter().any(|a| matches!(&a.content, AddedContent::Text(t) if t.text == "Typed")), "quit: {quit}"); + let _ = std::fs::remove_dir_all(&dir); + } + } + #[test] fn successful_revert_clears_focused_or_queued_drafts_only_on_its_document() { for queued in [false, true] {