diff --git a/crates/ui-egui/src/app/view.rs b/crates/ui-egui/src/app/view.rs index b016445bd..989514991 100644 --- a/crates/ui-egui/src/app/view.rs +++ b/crates/ui-egui/src/app/view.rs @@ -26,6 +26,7 @@ impl VectorcraftApp { view.artboard = view.artboard.min(doc.doc.artboards.len().saturating_sub(1)); } self.view_uids = docs.iter().map(|d| d.uid).collect(); + panels::layers::retain_open_thumbs(&self.view_uids); crate::doc_windows::retain_views(self); crate::doc_windows::opened(self); // Undo can remove the Artboard tool's active or additionally selected boards. Repair only diff --git a/crates/ui-egui/src/panels/layers.rs b/crates/ui-egui/src/panels/layers.rs index 2a737d2fb..98c082fc7 100644 --- a/crates/ui-egui/src/panels/layers.rs +++ b/crates/ui-egui/src/panels/layers.rs @@ -69,7 +69,7 @@ use model::{push_rows, rows_of_doc}; use rows::{drag_ghost, key, rename_id, resolve_click, row}; #[cfg(test)] use thumbs::THUMBS; -pub(crate) use thumbs::{ThumbSource, node_arc, real_thumb, show_thumbs_of, thumb_options}; +pub(crate) use thumbs::{ThumbSource, node_arc, real_thumb, retain_open_thumbs, show_thumbs_of, thumb_options}; /// Width of the eye and the lock columns. const COLUMN: f32 = 25.0; diff --git a/crates/ui-egui/src/panels/layers/tests.rs b/crates/ui-egui/src/panels/layers/tests.rs index 242e841c1..703d34553 100644 --- a/crates/ui-egui/src/panels/layers/tests.rs +++ b/crates/ui-egui/src/panels/layers/tests.rs @@ -246,6 +246,29 @@ fn a_layer_that_does_not_print_has_its_name_italic() { assert!(!italic_names(&mut app, &ctx).iter().any(|n| n == "Layer 1"), "Print on again: upright again"); } +#[test] +fn closing_a_document_releases_only_its_layer_thumbnails() { + let (mut app, _, _) = two_rects(); + let ctx = egui::Context::default(); + let first = app.session.active().unwrap().uid; + frame(&mut app, &ctx, vec![], false); + assert!(THUMBS.with(|c| c.borrow().cached(first))); + + app.session.execute("file.new", &json!({"width": 200, "height": 200})).unwrap(); + app.session.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap(); + let second = app.session.active().unwrap().uid; + frame(&mut app, &ctx, vec![], false); + assert!(THUMBS.with(|c| c.borrow().cached(second))); + + app.session.execute("file.close", &json!({"index": 0})).unwrap(); + app.sync_views(); + THUMBS.with(|c| { + let thumbs = c.borrow(); + assert!(!thumbs.cached(first), "closed document textures are released"); + assert!(thumbs.cached(second), "open document textures stay cached"); + }); +} + #[test] fn row_thumbnails_wait_for_the_end_of_a_drag() { let (mut app, _, _) = two_rects(); diff --git a/crates/ui-egui/src/panels/layers/thumbs.rs b/crates/ui-egui/src/panels/layers/thumbs.rs index 78e1217c2..c8b37eae7 100644 --- a/crates/ui-egui/src/panels/layers/thumbs.rs +++ b/crates/ui-egui/src/panels/layers/thumbs.rs @@ -1,3 +1,4 @@ +use std::collections::HashSet; use vectorcraft_doc::{NodeKind, TextKind}; use vectorcraft_geom::PathData; @@ -51,6 +52,8 @@ type ThumbJob = ((u64, NodeId), Weak, u32); #[derive(Default)] pub(super) struct Thumbs { entries: HashMap<(u64, NodeId), ThumbEntry>, + /// Documents still open. None until the app first synchronizes its documents. + open_docs: Option>, /// What the worker is rendering, by row. pending: HashMap<(u64, NodeId), (Weak, u32)>, worker: Option>, @@ -249,6 +252,36 @@ fn render_thumb(r: &mut vectorcraft_render::Renderer, doc: &Document, n: &Node, } impl Thumbs { + #[cfg(test)] + pub(super) fn cached(&self, uid: u64) -> bool { + self.entries.keys().any(|key| key.0 == uid) + } + + /// Release thumbnails and queued jobs for documents that have closed, keeping the cache for + /// other open documents even when the panels are hidden or another document is active. + fn retain_open(&mut self, uids: &[u64]) { + if self.open_docs.as_ref().is_some_and(|open| open.len() == uids.len() && uids.iter().all(|uid| open.contains(uid))) { + return; + } + let open: HashSet = uids.iter().copied().collect(); + let stale = + self.entries.keys().chain(self.pending.keys()).any(|key| !open.contains(&key.0)) || self.showing.is_some_and(|uid| !open.contains(&uid)); + self.entries.retain(|key, _| open.contains(&key.0)); + self.pending.retain(|key, _| open.contains(&key.0)); + self.showing = self.showing.filter(|uid| open.contains(uid)); + if stale + && let Some(w) = &self.worker + && !w.discard({ + let open = open.clone(); + move |key| !open.contains(&key.0) + }) + { + self.worker = None; + self.pending.clear(); + } + self.open_docs = Some(open); + } + /// Keep `img` (None: nothing drawn) as the thumbnail of row `key` showing `node`. fn upload(&mut self, ctx: &egui::Context, key: (u64, NodeId), node: Weak, px: u32, img: Option<&vectorcraft_render::Rendered>, ms: f64) { let color = img.map(crate::canvas::color_image); @@ -280,7 +313,7 @@ impl Thumbs { /// The panels draw the rows of document `uid` (None: no document). When that is another /// document than before, the worker drops the jobs it has not started for every other /// document, so they don't hold up this one's thumbnails, and those rows are asked for again - /// when their document shows again. The thumbnails made already stay. + /// when their document shows again. The thumbnails of still-open documents stay. pub(super) fn show(&mut self, uid: Option) { if self.showing == uid { return; @@ -298,6 +331,9 @@ impl Thumbs { /// Keep the thumbnail `d` the worker finished. fn receive(&mut self, ctx: &egui::Context, d: Done) { let (key, node, px) = d.key; + if self.open_docs.as_ref().is_some_and(|open| !open.contains(&key.0)) { + return; + } if self.pending.get(&key).is_some_and(|(p, ppx)| p.ptr_eq(&node) && *ppx == px) { self.pending.remove(&key); } @@ -383,6 +419,11 @@ pub(crate) fn show_thumbs_of(uid: Option) { THUMBS.with(|c| c.borrow_mut().show(uid)); } +/// Forget thumbnails of documents no longer open, even if neither thumbnail panel is shown. +pub(crate) fn retain_open_thumbs(uids: &[u64]) { + THUMBS.with(|c| c.borrow_mut().retain_open(uids)); +} + /// A real rendered thumbnail of row `n`, cached by node identity (unchanged nodes keep their `Arc` /// allocation, so the address is a free change detector). Only rendered for visible rows; see /// [`Thumbs::texture`] for when. @@ -440,6 +481,17 @@ mod tests { use super::*; + #[test] + fn a_finished_job_cannot_restore_a_closed_documents_thumbnail() { + let mut thumbs = Thumbs::default(); + let ctx = egui::Context::default(); + thumbs.retain_open(&[2]); + thumbs.receive(&ctx, Done { key: ((1, NodeId(1)), Weak::new(), 16), img: None, ms: 0.0 }); + assert!(thumbs.entries.is_empty(), "a closed document's late result is discarded"); + thumbs.receive(&ctx, Done { key: ((2, NodeId(2)), Weak::new(), 16), img: None, ms: 0.0 }); + assert!(thumbs.entries.contains_key(&(2, NodeId(2))), "an open document's result is kept"); + } + /// An open zigzag of `anchors` anchors. fn line(anchors: usize) -> PathData { let pts: Vec = (0..anchors).map(|i| Point::new(i as f64 * 0.01, (i % 2) as f64 * 50.0)).collect();