ElyPDF: only content edits keep old tiles and known sizes; old tiles paint and free with their tab (R2.6)

This commit is contained in:
ZacharyZhang-NY committed 2026-10-09 05:51:44 -04:00
1 parent 1bf693f7ab
commit 03bcbab5ce
11 files changed
+108 -26

No files matched your search

+1 -1
View File
@@ -72,7 +72,7 @@ impl Viewer {
let turned = unturn(margins, view.read(cx).rotate);
let points = turned.map(|side| side * MM);
view.update(cx, |view, cx| {
view.edit("cropping pages", move |worker, doc| worker.crop_pages(doc, &pages, points), cx)
view.edit_pages("cropping pages", move |worker, doc| worker.crop_pages(doc, &pages, points), cx)
});
cx.notify();
}
+31 -10
View File
@@ -17,6 +17,8 @@ pub struct Queued {
op: Op,
pick: Option<BTreeSet<usize>>,
doc: Doc,
/// The edit may add, drop, move or reshape pages.
pages: bool,
}
/// The document as an edit left it.
@@ -36,20 +38,31 @@ impl PdfView {
op: impl FnOnce(&mut Worker, Doc) -> io::Result<String> + Send + 'static,
cx: &mut Context<Self>,
) {
self.edit_picking(what, None, op, cx);
self.queue_edit(what, None, false, op, cx);
}
/// Queues an edit to the pages themselves.
pub fn edit_pages(
&mut self,
what: &'static str,
op: impl FnOnce(&mut Worker, Doc) -> io::Result<String> + Send + 'static,
cx: &mut Context<Self>,
) {
self.queue_edit(what, None, true, op, cx);
}
/// Queues `op`; once it lands, `pick` is picked.
pub fn edit_picking(
pub fn queue_edit(
&mut self,
what: &'static str,
pick: Option<BTreeSet<usize>>,
pages: bool,
op: impl FnOnce(&mut Worker, Doc) -> io::Result<String> + Send + 'static,
cx: &mut Context<Self>,
) {
log::info!("pdf: queues {what}");
// Aimed at what is shown now; later states refuse it.
self.queue.push_back(Queued { what, op: Box::new(op), pick, doc: self.doc });
self.queue.push_back(Queued { what, op: Box::new(op), pick, doc: self.doc, pages });
self.next_edit(cx);
}
@@ -57,7 +70,7 @@ impl PdfView {
if self.editing {
return;
}
let Some(Queued { what, op, pick, doc }) = self.queue.pop_front() else {
let Some(Queued { what, op, pick, doc, pages }) = self.queue.pop_front() else {
return;
};
self.editing = true;
@@ -73,7 +86,7 @@ impl PdfView {
view.editing = false;
match fresh {
Ok(fresh) => {
view.refresh(fresh, cx);
view.refresh(fresh, pages, cx);
if let Some(pick) = pick {
view.anchor = pick.first().copied();
view.picked = pick;
@@ -93,13 +106,21 @@ impl PdfView {
}
/// Everything read before the edit goes.
fn refresh(&mut self, fresh: Fresh, cx: &mut Context<Self>) {
fn refresh(&mut self, fresh: Fresh, pages: bool, cx: &mut Context<Self>) {
self.version += 1;
self.edited += 1;
(self.doc, self.mark) = (fresh.doc, fresh.mark);
// Old tiles stay under unchanged pages; nothing flashes.
self.keep_all();
self.sizes = sizes(fresh.pages, &fresh.first);
let measured = sizes(fresh.pages, &fresh.first);
if pages || fresh.pages != self.sizes.len() {
// A page may now stand where another stood.
self.retire_tiles();
self.sizes = measured;
} else {
// Same pages: old tiles stay under them until repainted.
self.keep_all();
let read = fresh.first.len();
self.sizes[..read].copy_from_slice(&measured[..read]);
}
self.marks = fresh.marks;
self.failed.clear();
self.text.clear();
@@ -145,6 +166,6 @@ impl PdfView {
pub fn undo(&mut self, redo: bool, cx: &mut Context<Self>) {
let what = if redo { "redo" } else { "undo" };
self.edit(what, move |worker, doc| worker.undo(doc, redo), cx);
self.edit_pages(what, move |worker, doc| worker.undo(doc, redo), cx);
}
}
+5 -5
View File
@@ -81,7 +81,7 @@ impl PdfView {
let pages = self.chosen_pages();
let before = pages.iter().filter(|page| **page < to).count();
let landed = (to - before..to - before + pages.len()).collect();
self.edit_picking("moving pages", Some(landed), move |worker, doc| worker.move_pages(doc, &pages, to), cx);
self.queue_edit("moving pages", Some(landed), true, move |worker, doc| worker.move_pages(doc, &pages, to), cx);
}
}
@@ -105,14 +105,14 @@ impl Viewer {
}
view.update(cx, |view, cx| {
view.picked.clear();
view.edit("deleting pages", move |worker, doc| worker.delete_pages(doc, &pages), cx);
view.edit_pages("deleting pages", move |worker, doc| worker.delete_pages(doc, &pages), cx);
});
}
fn turn_pages(&mut self, by: i32, cx: &mut Context<Self>) {
self.on_pages(cx, |view, cx| {
let pages = view.chosen_pages();
view.edit("turning pages", move |worker, doc| worker.rotate_pages(doc, &pages, by), cx);
view.edit_pages("turning pages", move |worker, doc| worker.rotate_pages(doc, &pages, by), cx);
});
}
@@ -136,7 +136,7 @@ impl Viewer {
self.on_pages(cx, |view, cx| {
let at = view.chosen_pages().last().copied().expect("one page at least");
let size = view.sizes[at];
view.edit("inserting a page", move |worker, doc| worker.insert_blank(doc, at + 1, size), cx);
view.edit_pages("inserting a page", move |worker, doc| worker.insert_blank(doc, at + 1, size), cx);
});
}
@@ -148,7 +148,7 @@ impl Viewer {
self.pick(false, cx, |viewer, path: PathBuf, cx| {
viewer.on_pages(cx, |view, cx| {
let at = view.chosen_pages().last().copied().expect("one page at least") + 1;
view.edit(
view.edit_pages(
"inserting pages",
move |worker, doc| worker.insert_file(doc, at, &path).map(|(_, mark)| mark),
cx,
+6 -5
View File
@@ -69,11 +69,12 @@ fn drawn_tile(image: Arc<RenderImage>, page: usize, view: gpui::Entity<PdfView>)
canvas(
|_, _, _| {},
move |bounds, _, window, cx| match window.paint_image(bounds, bounds, Default::default(), image, 0, false) {
Ok(()) if !view.read(cx).drawn => {
view.update(cx, |view, _| view.drawn = true);
log::info!("pdf: first tile drawn, page {page}");
}
Ok(()) => {}
Ok(()) => view.update(cx, |view, _| {
view.painted += 1;
if view.painted == 1 {
log::info!("pdf: first tile drawn, page {page}");
}
}),
Err(error) => log::error!("pdf: drawing a page failed: {error}"),
},
)
+43
View File
@@ -272,3 +272,46 @@ fn an_edit_past_the_first_hundred_sizes_keeps_the_page_drawn() {
settle(cx, |app| view.read(app).edited == 1 && repainted(view.read(app)));
assert!(!bare.get(), "the page never goes blank");
}
#[test]
fn an_edit_on_page_101_keeps_its_measured_size_and_its_tiles() {
let dir = tempfile::tempdir().expect("a temp dir");
let mut pages: Vec<(f32, f32, &[&str])> = vec![(612.0, 792.0, &["Page"]); 100];
pages.push((612.0, 900.0, &["Taller"]));
let mut cx = TestAppContext::single();
let (viewer, cx) = windowed(&mut cx, dir.path(), &[file(dir.path(), "mixed.pdf", &pages)], true);
let view = shown(cx, &viewer);
settle(cx, |app| view.read(app).sizes[100] == (612.0, 900.0));
cx.update(|_, app| view.update(app, |view, cx| view.show(100, 0.0, cx)));
settle(cx, |app| view.read(app).current == 100 && repainted(view.read(app)));
let (_watch, bare) = never_bare(cx, &view);
let late = ely_pdf_engine::NewAnnot { page: 100, ..note() };
cx.update(|_, app| view.update(app, |view, cx| view.annotate("adding a note", vec![late], cx)));
settle(cx, |app| view.read(app).edited == 1 && repainted(view.read(app)));
assert!(!bare.get(), "page 101 never goes blank");
assert_eq!(cx.update(|_, app| view.read(app).sizes[100]), (612.0, 900.0), "its measured size holds");
}
#[test]
fn old_tiles_are_painted_and_freed_with_their_tab() {
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| repainted(view.read(app)));
// Nothing new is asked; only old tiles can paint.
let before = cx.update(|_, app| {
view.update(app, |view, cx| {
let keys: Vec<_> = view.tiles.keys().copied().collect();
view.failed.extend(keys);
view.keep_all();
cx.notify();
view.painted
})
});
cx.run_until_parked();
let (painted, kept) = cx.update(|_, app| (view.read(app).painted, view.read(app).backdrop.len()));
assert!(kept > 0 && painted >= before + kept as u64, "{kept} old tiles, {} paints", painted - before);
cx.update(|_, app| view.update(app, |view, _| view.retire_all()));
assert!(cx.update(|_, app| view.read(app).backdrop.is_empty()), "closing hands every old tile over");
}
+5 -4
View File
@@ -37,8 +37,8 @@ pub struct PdfView {
pub tiles: HashMap<TileKey, Arc<RenderImage>>,
/// Images no longer drawn, freed at the next render.
pub retired: ely_gpui_component::primitives::Retired,
/// A tile reached the screen; logged once (PERF-05).
pub drawn: bool,
/// Tile images painted so far; the first is logged (PERF-05).
pub painted: u64,
/// Tiles the worker could not draw; not asked again.
pub failed: HashSet<TileKey>,
/// Old tiles shown under their page until covered.
@@ -145,7 +145,8 @@ impl PdfView {
pub fn retire_all(&mut self) -> ely_gpui_component::primitives::Retired {
let mut images = std::mem::take(&mut self.retired);
let tiles = self.tiles.drain().map(|(_, image)| image);
for image in tiles.chain(self.thumbs.drain().map(|(_, image)| image)) {
let old = self.backdrop.drain(..).map(|old| old.image);
for image in tiles.chain(old).chain(self.thumbs.drain().map(|(_, image)| image)) {
images.push(image);
}
images
@@ -166,7 +167,7 @@ impl PdfView {
scroll: ScrollHandle::new(),
tiles: HashMap::new(),
retired: Default::default(),
drawn: false,
painted: 0,
failed: HashSet::new(),
backdrop: Vec::new(),
text: HashMap::new(),