From 90c15aa514872e7dfa3e823326b58fbb9cd9cb19 Mon Sep 17 00:00:00 2001 From: Philip Date: Wed, 21 Oct 2026 21:43:58 +0100 Subject: [PATCH] M10: review of #1609: an automatic run's result waits for Read mode and dialogs CodeRabbit: Edit refuses to start recognition in Read mode or beneath a dialog, but a run started a moment earlier could finish after the user had switched to Read mode or opened a dialog, and poll_ocr applied its result there. The result of a run Edit started is now left until both are gone; a run the user asked for completes as before. The unit test uses a made-up result, so it needs no OCR models, and fails without the check. Co-Authored-By: Claude Sonnet 5.5 --- crates/ui-egui/src/ocr_ui.rs | 43 ++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/crates/ui-egui/src/ocr_ui.rs b/crates/ui-egui/src/ocr_ui.rs index 039384da80..186e5753c6 100644 --- a/crates/ui-egui/src/ocr_ui.rs +++ b/crates/ui-egui/src/ocr_ui.rs @@ -400,6 +400,16 @@ impl PdfCraftApp { let Some(run) = self.ocr_run.as_ref() else { return }; let (doc, progress, automatic) = (run.doc, run.progress.clone(), run.automatic); let Ok(mut s) = progress.lock() else { return }; + // The result of a run Edit started waits while Read mode or a dialog is up: nothing may change + // the document beneath them, and the run may have been started before they appeared. A run + // the user asked for completes as before. + if automatic && s.result.is_some() && (self.mode == crate::Mode::Read || self.modal_open()) { + drop(s); + if let Some(ctx) = &self.ctx { + ctx.request_repaint_after(std::time::Duration::from_millis(200)); + } + return; + } let Some(result) = s.result.take() else { let msg = crate::i18n::fmt( tl!("Recognizing text… page {d} of {t}"), @@ -539,4 +549,37 @@ mod tests { app.session.apply(id, add).unwrap(); assert_eq!(found(&mut app), Some(0), "a picture over the page makes it a scan"); } + /// A finished run Edit started is applied only once Read mode and dialogs are gone; one the user + /// asked for is applied at once. The result is made up, so no OCR models are needed. + #[test] + fn the_result_of_an_automatic_run_waits_for_read_mode_and_dialogs() { + let word = || pdfcraft_engine::ocr::PlacedWord { text: "scanned".into(), origin: [72.0, 700.0], across: [60.0, 0.0], up: [0.0, 12.0] }; + let finished = |app: &PdfCraftApp, automatic: bool| { + let page = pdfcraft_engine::ocr::OcrPage { page: 0, words: vec![word()], already_on_page: 0, skipped: None }; + let progress = Arc::new(Mutex::new(OcrProgress { done: 1, total: 1, result: Some(Ok(vec![page])), cancel: false })); + OcrRun { doc: app.active_ids().unwrap().1, progress, automatic } + }; + let label = |app: &PdfCraftApp| app.session.get(app.active_ids().unwrap().1).unwrap().can_undo().map(String::from); + + let mut app = app_with(scan()); + app.ocr_run = Some(finished(&app, true)); + app.mode = Mode::Read; + app.poll_ocr(); + assert!(app.ocr_run.is_some() && label(&app).is_none(), "waits in Read mode"); + app.mode = Mode::Edit; + app.dialog = Some(Dialog::JsConsole); + app.poll_ocr(); + assert!(app.ocr_run.is_some() && label(&app).is_none(), "waits beneath a dialog"); + app.dialog = None; + app.poll_ocr(); + assert!(app.ocr_run.is_none(), "taken once they are gone"); + assert_eq!(label(&app).as_deref(), Some("Recognize text")); + + // A run the user asked for is not held back. + let mut app = app_with(scan()); + app.ocr_run = Some(finished(&app, false)); + app.mode = Mode::Read; + app.poll_ocr(); + assert_eq!(label(&app).as_deref(), Some("Recognize text")); + } }