diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 6a107e14..d7351917 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -840,10 +840,6 @@ impl DocView { if self.selection.is_some_and(|s| s.page == page) { self.selection = None; } - if let Some(f) = self.find.as_mut() { - f.matches.retain(|(p, _)| *p != page); - f.current = None; - } } /// Pages drawn last frame and their screen rectangles. @@ -916,17 +912,16 @@ impl DocView { f.matches.retain(|(p, _)| *p != page); f.matches.extend(text.find_opts(&f.case_query, f.case_sensitive, f.whole_words).into_iter().map(|r| (page, r))); f.matches.sort_by_key(|(p, r)| (*p, r.start)); - f.current = match current_key { - Some(k) => f.matches.iter().position(|m| *m == k), - None => None, - }; + f.current = current_key.as_ref().and_then(|k| f.matches.iter().position(|m| m == k)); if f.current.is_none() && !f.matches.is_empty() { // First match at or after the page being viewed. let cur = self.current; let i = f.matches.iter().position(|(p, _)| *p >= cur).unwrap_or(0); f.current = Some(i); let (p, _) = f.matches[i]; - self.flash_match(p); + if current_key.is_none() { + self.flash_match(p); + } } } @@ -1548,6 +1543,11 @@ impl DocView { None => { log::warn!("page {} text: {}", r.request.page + 1, r.error.unwrap_or_default()); self.text_failed.insert(r.request.page); + if let Some(f) = self.find.as_mut() { + let current_key = f.current.and_then(|c| f.matches.get(c).cloned()); + f.matches.retain(|(p, _)| *p != r.request.page); + f.current = current_key.and_then(|k| f.matches.iter().position(|m| *m == k)); + } } } return 0; diff --git a/crates/ui-egui/tests/viewer.rs b/crates/ui-egui/tests/viewer.rs index fa32dc8c..3a62aa5b 100644 --- a/crates/ui-egui/tests/viewer.rs +++ b/crates/ui-egui/tests/viewer.rs @@ -169,6 +169,63 @@ fn find_shortcuts_advance_and_retreat_matches() { assert_eq!(h.state().views[0].find.as_ref().unwrap().current, Some((start + n - 1) % n)); } +#[test] +fn marking_while_searching_does_not_jitter_or_reset_search_state() { + let mut h = harness(); + h.state_mut().active = Some(0); + h.run_steps(2); + h.state_mut().views[0].open_find(); + h.state_mut().views[0].find.as_mut().unwrap().query = "page".into(); + h.state_mut().views[0].rerun_find(); + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(20); + while std::time::Instant::now() < deadline { + h.run_steps(2); + std::thread::sleep(std::time::Duration::from_millis(5)); + if h.state().views[0].find.as_ref().unwrap().matches.len() >= 15 { + break; + } + } + assert_eq!(h.state().views[0].find.as_ref().unwrap().matches.len(), 15); + let current_before = h.state().views[0].find.as_ref().unwrap().current; + assert!(current_before.is_some()); + // Clear any pending goto from initial find + h.run_steps(2); + assert!(h.state().views[0].goto.is_none()); + + // User draws or marks an annotation on page 0 + let shape = pdfcraft_engine::Shape::Ink { + strokes: vec![vec![[10.0, 10.0], [20.0, 20.0]]], + }; + let stroke = pdfcraft_engine::Edit::AddAnnotation(pdfcraft_engine::NewAnnotation { + page: 0, + style: pdfcraft_engine::Style::default_for(&shape), + shape, + contents: String::new(), + author: "tester".into(), + }); + h.state_mut().apply_edit(stroke); + + // Right after applying the edit, search matches and current match must still be valid + assert_eq!( + h.state().views[0].find.as_ref().unwrap().current, + current_before, + "applying an annotation must not wipe current search match" + ); + + // Let the canvas process the reload + let reload_deadline = std::time::Instant::now() + std::time::Duration::from_secs(5); + while std::time::Instant::now() < reload_deadline { + h.run_steps(2); + std::thread::sleep(std::time::Duration::from_millis(5)); + assert!(h.state().views[0].goto.is_none(), "page reload after annotation must not set goto / trigger display jitter"); + if h.state().views[0].glyph_screen_pos(0, 0).is_some() { + break; + } + } + assert_eq!(h.state().views[0].find.as_ref().unwrap().current, current_before); + assert_eq!(h.state().views[0].find.as_ref().unwrap().matches.len(), 15); +} + #[test] fn layouts_fit_height_labels_and_system_theme() { use pdfcraft_ui_egui::canvas::{Fit, PageLayout};