From 9fe28f072b377c5f427e6f181a64d033126e8db3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Fri, 9 Oct 2026 02:34:32 -0400 Subject: [PATCH] ElyPDF: an edit keeps the page drawn until its repaint lands (R2.6) --- apps/ely-pdf/src/edits.rs | 10 +++++++-- apps/ely-pdf/src/pages.rs | 5 +++++ apps/ely-pdf/src/tests/input.rs | 39 +++++++++++++++++++++++++++++++++ apps/ely-pdf/src/view.rs | 4 ++++ 4 files changed, 56 insertions(+), 2 deletions(-) diff --git a/apps/ely-pdf/src/edits.rs b/apps/ely-pdf/src/edits.rs index 86362ac..ca29484 100644 --- a/apps/ely-pdf/src/edits.rs +++ b/apps/ely-pdf/src/edits.rs @@ -97,9 +97,15 @@ impl PdfView { self.version += 1; self.edited += 1; (self.doc, self.mark) = (fresh.doc, fresh.mark); - self.sizes = sizes(fresh.pages, &fresh.first); + let sizes = sizes(fresh.pages, &fresh.first); + // Same pages: old tiles stay until repainted, so nothing flashes. + if sizes == self.sizes { + self.stale.extend(self.tiles.keys().copied()); + } else { + self.retire_tiles(); + } + self.sizes = sizes; self.marks = fresh.marks; - self.retire_tiles(); self.failed.clear(); self.text.clear(); self.asking_text.clear(); diff --git a/apps/ely-pdf/src/pages.rs b/apps/ely-pdf/src/pages.rs index 04cd2eb..74e079a 100644 --- a/apps/ely-pdf/src/pages.rs +++ b/apps/ely-pdf/src/pages.rs @@ -164,6 +164,9 @@ impl PdfView { let key = TileKey { page: place.page, scale, rotate: self.rotate, dark, tx, ty }; let rect = [tx * TILE, ty * TILE, TILE.min(dw - tx * TILE), TILE.min(dh - ty * TILE)]; seen.insert(key); + if self.stale.contains(&key) && !self.failed.contains(&key) { + wanted.push((key, rect)); + } match self.tiles.get(&key) { Some(image) => tiles.push( drawn_tile(image.clone(), place.page, cx.entity()) @@ -190,6 +193,7 @@ impl PdfView { } kept }); + self.stale.retain(|key| seen.contains(key)); let pages_shown: Vec = shown.iter().map(|place| place.page).collect(); self.ask_layers(&pages_shown, cx); self.ask_notes(pages_shown.iter().copied(), cx); @@ -295,6 +299,7 @@ impl PdfView { match painted.map(|bgra| picture((rect[2] as u32, rect[3] as u32), bgra)) { Ok(Some(image)) => { log::debug!("pdf: a tile of page {} came", key.page); + view.stale.remove(&key); if let Some(old) = view.tiles.insert(key, image) { view.retired.push(old); } diff --git a/apps/ely-pdf/src/tests/input.rs b/apps/ely-pdf/src/tests/input.rs index 0ef77f7..034cec5 100644 --- a/apps/ely-pdf/src/tests/input.rs +++ b/apps/ely-pdf/src/tests/input.rs @@ -185,3 +185,42 @@ fn tiles_beside_the_view_are_neither_asked_nor_kept() { })); settle(cx, |app| columns(app).contains(&last) && !columns(app).contains(&0)); } + +#[test] +fn an_edit_keeps_the_page_drawn_until_its_repaint_lands() { + let dir = tempfile::tempdir().expect("a temp dir"); + let mut cx = TestAppContext::single(); + let (viewer, cx) = windowed(&mut cx, dir.path(), &[file(dir.path(), "one.pdf", &[(612.0, 792.0, &["Hello"])])], true); + let view = shown(cx, &viewer); + settle(cx, |app| !view.read(app).tiles.is_empty() && !view.read(app).painting); + let before: Vec<_> = cx.update(|_, app| view.read(app).tiles.values().cloned().collect()); + let note = ely_pdf_engine::NewAnnot { + page: 0, + shape: ely_pdf_engine::Shape::Note((100.0, 500.0)), + rgb: None, + contents: "Look".into(), + author: String::new(), + }; + // Watches every notify, the edit's own included, before repaints. + let blank = std::rc::Rc::new(std::cell::Cell::new(false)); + let seen = blank.clone(); + let _watch = cx.update(|_, app| { + app.observe(&view, move |view, app| { + let read = view.read(app); + if read.edited == 1 && read.tiles.is_empty() { + seen.set(true); + } + }) + }); + cx.update(|_, app| view.update(app, |view, cx| view.annotate("adding a note", vec![note], cx))); + settle(cx, |app| { + let read = view.read(app); + read.edited == 1 && read.stale.is_empty() && !read.painting + }); + assert!(!blank.get(), "the page never goes blank"); + let after = cx.update(|_, app| view.read(app).tiles.values().cloned().collect::>()); + assert!( + after.iter().all(|image| before.iter().all(|old| !std::sync::Arc::ptr_eq(old, image))), + "every tile was painted again" + ); +} diff --git a/apps/ely-pdf/src/view.rs b/apps/ely-pdf/src/view.rs index 2d67a21..1d1d2e6 100644 --- a/apps/ely-pdf/src/view.rs +++ b/apps/ely-pdf/src/view.rs @@ -41,6 +41,8 @@ pub struct PdfView { pub drawn: bool, /// Tiles the worker could not draw; not asked again. pub failed: HashSet, + /// Tiles from before an edit: shown until their repaint lands. + pub stale: HashSet, pub text: HashMap>>, pub asking_text: HashSet, pub marks: Vec, @@ -166,6 +168,7 @@ impl PdfView { retired: Default::default(), drawn: false, failed: HashSet::new(), + stale: HashSet::new(), text: HashMap::new(), asking_text: HashSet::new(), marks: loaded.marks, @@ -311,6 +314,7 @@ impl PdfView { /// Every page tile goes; images are freed next render. pub fn retire_tiles(&mut self) { + self.stale.clear(); for (_, image) in self.tiles.drain() { self.retired.push(image); }