Merge pull request #5795 from dotCooCoo/fix/layers-thumbs-close-cache

Release Layers thumbnails when a document closes
This commit is contained in:
BFlat
2026-11-05 20:30:52 +07:00
committed by GitHub
4 changed files with 78 additions and 2 deletions
+1
View File
@@ -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
+1 -1
View File
@@ -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;
+23
View File
@@ -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();
+53 -1
View File
@@ -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<Node>, 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<HashSet<u64>>,
/// What the worker is rendering, by row.
pending: HashMap<(u64, NodeId), (Weak<Node>, u32)>,
worker: Option<ThumbWorker<(u64, NodeId), ThumbJob>>,
@@ -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<u64> = 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<Node>, 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<u64>) {
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<ThumbJob>) {
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<u64>) {
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<Point> = (0..anchors).map(|i| Point::new(i as f64 * 0.01, (i % 2) as f64 * 50.0)).collect();