From 835659131e9b6ee29332a0b6b977cc2bef6aeefe Mon Sep 17 00:00:00 2001 From: Greg Pomerantz Date: Wed, 28 Oct 2026 13:31:47 -0400 Subject: [PATCH] No reload flash when the rescan finds nothing new MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The scan's final set_list replaced the whole model unconditionally: generation bump + painted-state clear + set_vec, so Slint recreated every row and every visible thumbnail flashed out and back — even when the rescan changed nothing (the normal warm start). Now: - unchanged list -> status line only, model untouched - displayed list is a prefix of the final one (cold-start fill) -> append the tail rows in place (no row recreation) - genuine change -> full replacement (the correct signal there) --- src/lib.rs | 46 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index fcd2adc..d5bbc91 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1184,10 +1184,50 @@ fn scan_thread(weak: &slint::Weak
) { let n = entries.len(); let ms = t0.elapsed().as_millis(); log::info!("gallery: scan {n} files ({ins}+ {upd}~ {del}-) in {ms} ms"); + let status = format!("{n} files ({ins}+ {upd}~ {del}–) in {ms} ms"); - // Final list: set_list bumps the generation + re-anchors the viewport; - // the next tick fills it (warm start: disk-cache hits, nearly free). - set_list(weak, entries, format!("{n} files ({ins}+ {upd}~ {del}–) in {ms} ms")); + // Scan finished: touch the model ONLY if what's on screen actually + // changed. A full set_list bumps the generation (drops in-flight + + // painted state) and replaces the list model — Slint recreates every + // row and every visible thumbnail flashes out and back, even when the + // rescan found nothing new. The normal warm start must be a no-op. + let same = |a: &Entry, b: &Entry| a.path == b.path && a.mtime == b.mtime && a.size == b.size; + let cur: Vec = state().entries.lock().unwrap().clone(); + if cur.len() == entries.len() && cur.iter().zip(&entries).all(|(a, b)| same(a, b)) { + log::info!("gallery: rescan unchanged — keeping live list (no reload)"); + set_status(weak, status); + return; + } + // Progressive-fill case (cold start): the displayed list is the final + // list's prefix — append the tail in place instead of replacing (no row + // recreation, no flash, no generation bump). + if entries.len() > cur.len() + && entries[..cur.len()].iter().zip(&cur).all(|(a, b)| same(a, b)) + { + let tail: Vec = entries[cur.len()..].to_vec(); + let full = entries; + log::info!( + "gallery: rescan added {} — extending list in place", + tail.len() + ); + let weak = weak.clone(); + let _ = slint::invoke_from_event_loop(move || { + let Some(w) = weak.upgrade() else { return }; + let Some(app) = STATE.get() else { return }; + *app.entries.lock().unwrap() = full; + if let Some(m) = w.get_items().as_any().downcast_ref::>() { + let at = m.row_count(); + for (i, it) in chunk(&tail).into_iter().enumerate() { + m.insert(at + i, it); + } + } + w.set_status(status.into()); + }); + return; + } + // Genuine change (files added mid-list, modified, removed): full + // replacement — the one case where the flash is the correct signal. + set_list(weak, entries, status); } /// A clone of `AndroidApp` kept for the app's whole lifetime so the Java side