From 68e5e6713b6027a07df810c5d433edb4d265b5cc Mon Sep 17 00:00:00 2001 From: MikeeI Date: Mon, 5 Oct 2026 07:23:55 +0200 Subject: [PATCH] fix(ui): release orphaned layer and mask thumbnails (#119) Sweep live layer and mask keys only after snapshot or document membership changes. Retain the union across open documents because native files can restore IDs; undo recreates missing handles. --- crates/ui-egui/src/lib.rs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 11d57d891f..4b07e65fdb 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -213,6 +213,8 @@ pub struct PhotocraftApp { pub fps: f32, last_frame_time: f64, thumbs: HashMap<(photocraft_doc::LayerId, bool), (u64, egui::TextureHandle)>, + /// Snapshots whose live layer/mask keys were last used to prune thumbnail handles. + thumb_documents: Vec<(DocId, std::sync::Weak)>, /// Content bounds cached per (key, revision): scanning a 36 MP layer every frame cost ~77 ms. bounds_cache: HashMap, /// Downsampled proxy of the active document for live previews: (doc, revision, k, proxy). @@ -308,6 +310,7 @@ impl PhotocraftApp { fps: 0.0, last_frame_time: 0.0, thumbs: HashMap::new(), + thumb_documents: Vec::new(), bounds_cache: HashMap::new(), proxy: None, proxy_uploaded: None, @@ -420,6 +423,7 @@ impl PhotocraftApp { let n = self.session.documents().len(); self.ui.views.resize_with(n, Default::default); self.ui.windows.retain(|w| w.document < n); + self.prune_thumbs(); } /// Record `path` as the most-recently-opened file (File › Open Recent): de-duplicated, newest @@ -768,6 +772,34 @@ fn read_dropped(_f: &dyn egui::DroppedFile) -> Result, String> { } impl PhotocraftApp { + fn prune_thumbs(&mut self) { + let documents = self.session.documents(); + if self.thumb_documents.len() == documents.len() + && self.thumb_documents.iter().zip(documents).all(|((id, snapshot), st)| *id == st.doc.id && snapshot.ptr_eq(&std::sync::Arc::downgrade(&st.doc))) + { + return; + } + // Check snapshots every sync, but walk the layer tree only after it changes. A Weak + // tracks replacement (including undo and preserved-ID reopen) without retaining pixels. + if !self.thumbs.is_empty() { + let mut live = std::collections::HashSet::new(); + let mut pending: Vec<_> = documents.iter().flat_map(|st| &st.doc.layers).collect(); + while let Some(layer) = pending.pop() { + live.insert((layer.id, false)); + if layer.mask.is_some() { + live.insert((layer.id, true)); + } + if let Some(children) = layer.children() { + pending.extend(children); + } + } + // Native files can restore IDs: retain the union across all open documents, rather + // than deleting an ID just because one document containing it was closed. + self.thumbs.retain(|key, _| live.contains(key)); + } + self.thumb_documents = documents.iter().map(|st| (st.doc.id, std::sync::Arc::downgrade(&st.doc))).collect(); + } + pub fn set_theme(&mut self, ctx: &egui::Context, kind: theme::ThemeKind) { self.ui.theme = kind; theme::apply(ctx, kind);