From d2d5d6f22bd87d3588d3b7f9e17fe4203bb17fad Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Mon, 17 Aug 2026 22:30:37 +0200 Subject: [PATCH] Keep the selection visible when the grid scrolls under it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Scrolling rebuilds every cell, and a fresh cell carries `selected: false`. `sync_badges` and `sync_ratings` refilled what the rebuild cleared; nothing refilled the selection, so the ticks vanished on every scroll. The selection itself was never lost — it is a set of image ids and survives untouched — which made this worse than losing it: the header buttons still acted on forty photographs the user could no longer see were held. `load_window` reaches the selection through a `Weak` handle set at wiring. Weak because the two controllers are joined only through the window, and an `Rc` each way would leak both; absent, the grid draws nothing selected, which is what it did before. Co-Authored-By: Claude Opus 5 --- ui/dr-ui/src/library_ui.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/ui/dr-ui/src/library_ui.rs b/ui/dr-ui/src/library_ui.rs index f7265ea..89fa881 100644 --- a/ui/dr-ui/src/library_ui.rs +++ b/ui/dr-ui/src/library_ui.rs @@ -121,6 +121,14 @@ pub struct LibraryController { /// A `Cell` because it is a `bool` read inside callbacks that already hold /// other borrows. viewing_trash: std::cell::Cell, + /// The selection's owner, so a rebuilt window can restore the `selected` + /// flags it just cleared. + /// + /// `Weak` because the two controllers outlive each other only through the + /// window, and an `Rc` both ways would leak both. Set once at wiring; a + /// `None` here means the grid simply draws nothing selected, which is the + /// old behaviour rather than a crash. + coll_ctl: RefCell>>, /// Timeline view state: how far zoomed in, and around what instant. /// /// Zoom is a level rather than a span so the axis halves and doubles in @@ -255,6 +263,7 @@ impl LibraryController { session: RefCell::new(None), scope: RefCell::new(None), viewing_trash: std::cell::Cell::new(false), + coll_ctl: RefCell::new(None), timeline_zoom: RefCell::new(0), timeline_centre: RefCell::new(None), pinch_accum: RefCell::new(1.0), @@ -1730,6 +1739,14 @@ fn load_window(window: &AppWindow, ctl: &Rc) { .collect(); crate::collections_ui::sync_badges(window, catalog, &ids); sync_ratings(window, catalog, &ids); + // The rebuilt cells all carry `selected: false`, but the selection itself + // is a set of image ids and survives untouched. Without this the ticks + // vanished on every scroll — the selection was still there and still acted + // on, which is worse than losing it, because the user cannot see what the + // buttons are about to do. + if let Some(coll) = ctl.coll_ctl.borrow().as_ref().and_then(|w| w.upgrade()) { + crate::collections_ui::sync_selection(window, &coll, &ids); + } // The filter chips' counts describe the whole library, not this window, so // they are refreshed here rather than per cell. refresh_rating_counts(window, catalog); @@ -3117,6 +3134,10 @@ pub fn wire( ) where F: Fn(String) + 'static, { + // So a rebuilt window can put the selection ticks back. Weak, or the two + // controllers would hold each other alive for the life of the process. + *ctl.coll_ctl.borrow_mut() = Some(Rc::downgrade(&coll_ctl)); + // Shared rather than moved: a click and `Return` both open an image, and // they are two callbacks. let on_open_image = Rc::new(on_open_image);