Move the library grid's cells and selection onto their own Slint global

AppWindow carried the grid's loaded window of cells, the keyboard cursor,
drag and drop, the held-row long-press state, columns and cell size, the
scroll and viewport bookkeeping, the photo roll's pick and centre-request,
and the local-only/reorder/collection-filing gestures that act on a
selection, as properties and callbacks on the root component. That state now
lives in the `Library` global declared in library.slint, next to the structs
(LibraryCell, TimelineBar, KeywordRow, PersonChip) it and the grid's other
components already share; Rust reaches it through
window.global::<Library>() instead of window.set_/get_/on_/invoke_ on the
root, the same change collections.slint's `Collections` global made for the
sidebar.

library-visible stays on AppWindow: it is computed from active-page and
active-view, the shell's own routing state, which a global cannot read.
Everything else still prefixed library- — the timeline, the filter bar,
ratings and flags, keywording, and the routes and status lines — stays on
the window for now and moves in the commits that follow.
This commit is contained in:
2026-09-20 21:42:48 +02:00
parent 3bfa73d1e1
commit 402dcdc24c
18 changed files with 992 additions and 900 deletions
+15 -11
View File
@@ -14,7 +14,7 @@ use std::sync::mpsc::Receiver;
use slint::{ComponentHandle, Model as _};
use crate::library::{self, ThumbnailMessage};
use crate::{AppWindow, LibraryCell};
use crate::{AppWindow, Library, LibraryCell};
use super::controller::{stop, LibraryController, LibraryFacts};
use super::filter_bar::period_headings;
@@ -245,8 +245,8 @@ pub(super) fn load_window(window: &AppWindow, ctl: &Rc<LibraryController>) {
// Read before it is overwritten: the property still holds what the library
// was last time this ran, and a library that has become shorter is the
// signal that something was deleted out from under the view.
let was = window.get_library_total().max(0) as usize;
window.set_library_total(total as i32);
let was = window.global::<Library>().get_library_total().max(0) as usize;
window.global::<Library>().set_library_total(total as i32);
let shrank = total < was;
// Clamp so a scrub to the very end still fills the window rather than
@@ -278,7 +278,7 @@ pub(super) fn load_window(window: &AppWindow, ctl: &Rc<LibraryController>) {
//
// The trash is never reorderable. It is a view of what was deleted, ordered
// by when, and it is not a collection at all.
window.set_library_reorderable(
window.global::<Library>().set_library_reorderable(
!trash
&& scope.is_some_and(|c| {
dr_catalog::collections::orders_manually(catalog.connection(), c).unwrap_or(false)
@@ -321,7 +321,7 @@ pub(super) fn load_window(window: &AppWindow, ctl: &Rc<LibraryController>) {
// Month headings. The grid is ordered by capture time, so without these a
// wall of thumbnails gives no sense of *when* you are looking — the
// sidebar says it, but only if you consult it.
let columns = window.get_library_columns().max(1) as usize;
let columns = window.global::<Library>().get_library_columns().max(1) as usize;
let headings = period_headings(
cells.iter().map(|c| {
c.captured_at.map(|t| {
@@ -335,7 +335,7 @@ pub(super) fn load_window(window: &AppWindow, ctl: &Rc<LibraryController>) {
// What the outgoing model is still holding — see [`hold_thumbnails`].
let held = {
let previous = window.get_library_cells();
let previous = window.global::<Library>().get_library_cells();
let ids = ctl.image_ids.borrow();
let classes = ctl.thumb_class.borrow();
hold_thumbnails(&previous, &ids, &classes)
@@ -391,7 +391,9 @@ pub(super) fn load_window(window: &AppWindow, ctl: &Rc<LibraryController>) {
// those fetches would have filled are absent from `requested` above, so the
// batch started below asks for them again.
ctl.generation.set(ctl.generation.get().wrapping_add(1));
window.set_library_cells(slint::ModelRc::new(slint::VecModel::from(rows)));
window
.global::<Library>()
.set_library_cells(slint::ModelRc::new(slint::VecModel::from(rows)));
// The model is fresh, so the "in this many collections" badges are all zero
// until refilled. One query for the whole window, not one per cell.
@@ -481,7 +483,7 @@ fn request_thumbnails(window: &AppWindow, ctl: &Rc<LibraryController>) {
// The drawn cell size decides which class to ask for. Chosen once for the
// batch rather than per row, and carried through to the drain so a cell it
// fills can record what it is now showing.
let cell_pixels = window.get_library_cell_size().max(1.0) as u32;
let cell_pixels = window.global::<Library>().get_library_cell_size().max(1.0) as u32;
let class = dr_thumbs::ThumbSize::for_cell(cell_pixels);
let mut wanted: Vec<library::ThumbnailRequest> = {
@@ -602,7 +604,9 @@ pub fn refresh_thumbnail(
// What the grid is drawing at, so the cell can be corrected on screen
// rather than only on disk.
let drawn = dr_thumbs::ThumbSize::for_cell(window.get_library_cell_size().max(1.0) as u32);
let drawn = dr_thumbs::ThumbSize::for_cell(
window.global::<Library>().get_library_cell_size().max(1.0) as u32,
);
for class in [dr_thumbs::ThumbSize::Grid, dr_thumbs::ThumbSize::Large] {
let (w, h, rgba) = match render(class.edge()) {
@@ -634,7 +638,7 @@ pub fn refresh_thumbnail(
// Straight into the model, so the edit is on the cell the moment
// the grid comes back rather than after a scroll evicts and
// refetches it.
let model = window.get_library_cells();
let model = window.global::<Library>().get_library_cells();
if let Some(mut cell) = model.row_data(row) {
cell.thumbnail = to_slint_image(w, h, &rgba);
cell.has_thumb = true;
@@ -707,7 +711,7 @@ fn drain_thumbnails(
return;
}
let model = w.get_library_cells();
let model = w.global::<Library>().get_library_cells();
loop {
let msg = match rx.try_recv() {