Keep the selection visible when the grid scrolls under it

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 <noreply@anthropic.com>
This commit is contained in:
2026-08-17 22:30:37 +02:00
co-authored by Claude Opus 5
parent 02d629922f
commit d2d5d6f22b
+21
View File
@@ -121,6 +121,14 @@ pub struct LibraryController {
/// A `Cell` because it is a `bool` read inside callbacks that already hold /// A `Cell` because it is a `bool` read inside callbacks that already hold
/// other borrows. /// other borrows.
viewing_trash: std::cell::Cell<bool>, viewing_trash: std::cell::Cell<bool>,
/// 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<Option<std::rc::Weak<crate::collections_ui::CollectionsController>>>,
/// Timeline view state: how far zoomed in, and around what instant. /// 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 /// 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), session: RefCell::new(None),
scope: RefCell::new(None), scope: RefCell::new(None),
viewing_trash: std::cell::Cell::new(false), viewing_trash: std::cell::Cell::new(false),
coll_ctl: RefCell::new(None),
timeline_zoom: RefCell::new(0), timeline_zoom: RefCell::new(0),
timeline_centre: RefCell::new(None), timeline_centre: RefCell::new(None),
pinch_accum: RefCell::new(1.0), pinch_accum: RefCell::new(1.0),
@@ -1730,6 +1739,14 @@ fn load_window(window: &AppWindow, ctl: &Rc<LibraryController>) {
.collect(); .collect();
crate::collections_ui::sync_badges(window, catalog, &ids); crate::collections_ui::sync_badges(window, catalog, &ids);
sync_ratings(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 // The filter chips' counts describe the whole library, not this window, so
// they are refreshed here rather than per cell. // they are refreshed here rather than per cell.
refresh_rating_counts(window, catalog); refresh_rating_counts(window, catalog);
@@ -3117,6 +3134,10 @@ pub fn wire<F>(
) where ) where
F: Fn(String) + 'static, 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 // Shared rather than moved: a click and `Return` both open an image, and
// they are two callbacks. // they are two callbacks.
let on_open_image = Rc::new(on_open_image); let on_open_image = Rc::new(on_open_image);