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
+11 -9
View File
@@ -12,7 +12,7 @@ use std::rc::Rc;
use slint::ComponentHandle;
use crate::library;
use crate::{AppWindow, PersonChip, View};
use crate::{AppWindow, Library, PersonChip, View};
use super::controller::LibraryController;
use super::offline::{
@@ -384,14 +384,16 @@ pub(super) fn wire_filter_scope_and_offline(
{
let weak = window.as_weak();
let ctl = ctl.clone();
window.on_library_toggle_local_only(move || {
let Some(w) = weak.upgrade() else { return };
let on = !ctl.local_only();
ctl.set_local_only(on);
ctl.filter.borrow_mut().local_only = on;
w.set_library_local_only(on);
refilter(&w, &ctl);
});
window
.global::<Library>()
.on_library_toggle_local_only(move || {
let Some(w) = weak.upgrade() else { return };
let on = !ctl.local_only();
ctl.set_local_only(on);
ctl.filter.borrow_mut().local_only = on;
w.global::<Library>().set_library_local_only(on);
refilter(&w, &ctl);
});
}
// TRACES: FR-NC-6a
+246 -226
View File
@@ -12,7 +12,7 @@ use std::rc::Rc;
use slint::{ComponentHandle, Model as _};
use crate::AppWindow;
use crate::{GestureRow, View};
use crate::{GestureRow, Library, View};
use super::controller::{
LibraryController, OpenImage, MAX_CELL_SIZE, MIN_CELL_SIZE, MIN_WINDOW, SCREENFULS,
@@ -49,7 +49,7 @@ fn move_cursor(
delta: i32,
extend: bool,
) {
let total = window.get_library_total().max(0) as usize;
let total = window.global::<Library>().get_library_total().max(0) as usize;
if total == 0 {
return;
}
@@ -93,11 +93,11 @@ fn place_cursor(
// quarter in, exactly as a scroll does it, so continuing in the same
// direction has loaded cells to move into rather than another reload on
// the very next press.
let loaded = window.get_library_cells().row_count();
let loaded = window.global::<Library>().get_library_cells().row_count();
let offset = *ctl.offset.borrow();
if at < offset || at >= offset + loaded {
let size = *ctl.window.borrow();
let total = window.get_library_total().max(0) as usize;
let total = window.global::<Library>().get_library_total().max(0) as usize;
*ctl.offset.borrow_mut() = window_start(at, size, total);
load_window(window, ctl);
}
@@ -122,7 +122,7 @@ fn place_cursor(
}
crate::collections_ui::select_row(window, coll, &ids, offset, row, false, extend);
window.set_library_cursor(at as i32);
window.global::<Library>().set_library_cursor(at as i32);
}
/// Say where in the library the photograph now open sits.
@@ -136,7 +136,7 @@ fn place_cursor(
fn report_position(window: &AppWindow, ctl: &Rc<LibraryController>, row: usize) {
let offset = *ctl.offset.borrow();
window.set_index((offset + row) as i32);
window.set_total(window.get_library_total());
window.set_total(window.global::<Library>().get_library_total());
}
/// Connect the grid's callbacks.
@@ -175,7 +175,9 @@ pub fn wire<F>(
// claims the gesture before the delay it would forward after. Seeding it
// here means the stars are on screen before the first touch rather than
// after it, which is the whole point of showing them.
window.set_library_touched(cfg!(target_os = "android"));
window
.global::<Library>()
.set_library_touched(cfg!(target_os = "android"));
// TRACES: FR-UI-4
// The gesture reference. Pushed once, here, rather than on demand: the
@@ -209,42 +211,44 @@ pub fn wire<F>(
let ctl = ctl.clone();
let coll_for_click = coll_ctl.clone();
let on_open_image = on_open_image.clone();
window.on_library_cell_clicked(move |i| {
let Some(w) = weak.upgrade() else { return };
window
.global::<Library>()
.on_library_cell_clicked(move |i| {
let Some(w) = weak.upgrade() else { return };
// The press stayed put, so it was a tap and not a drag: whatever it
// held back can be applied now. See `collections_ui::Press`.
crate::collections_ui::commit_press(&w, &coll_for_click, &ctl.visible_ids());
// The press stayed put, so it was a tap and not a drag: whatever it
// held back can be applied now. See `collections_ui::Press`.
crate::collections_ui::commit_press(&w, &coll_for_click, &ctl.visible_ids());
// A ctrl- or shift-click is a selection gesture. Opening the image
// too would throw the user out of the grid mid-selection.
if coll_for_click.press_was_modified() {
return;
}
// A ctrl- or shift-click is a selection gesture. Opening the image
// too would throw the user out of the grid mid-selection.
if coll_for_click.press_was_modified() {
return;
}
let path = ctl.paths.borrow().get(i as usize).cloned();
if let Some(path) = path {
// Leave the grid for the develop view. The status bar's
// "‹ Library" button comes back here.
w.set_active_view(View::Develop);
// Which cell the develop view is now showing, so the photo
// roll opens marking it rather than marking nothing.
w.set_library_roll_current(i);
// A develop session begins here, so the roll centres on this
// photograph the first time it settles rather than merely
// scrolling it into view at one edge. Raised here and not in
// `on_library_roll_pick`, which is a step *within* a session —
// see `PhotoRoll::centre-request`.
w.set_library_roll_centre(true);
on_open_image(path);
report_position(&w, &ctl, i as usize);
// TRACES: FR-UI-8
// Written now rather than on a settle: which view you are in is
// the field a debounce is most likely to lose, and quitting
// straight from develop is exactly the case worth getting right.
write_place(&w, &ctl);
}
});
let path = ctl.paths.borrow().get(i as usize).cloned();
if let Some(path) = path {
// Leave the grid for the develop view. The status bar's
// "‹ Library" button comes back here.
w.set_active_view(View::Develop);
// Which cell the develop view is now showing, so the photo
// roll opens marking it rather than marking nothing.
w.global::<Library>().set_library_roll_current(i);
// A develop session begins here, so the roll centres on this
// photograph the first time it settles rather than merely
// scrolling it into view at one edge. Raised here and not in
// `on_library_roll_pick`, which is a step *within* a session —
// see `PhotoRoll::centre-request`.
w.global::<Library>().set_library_roll_centre(true);
on_open_image(path);
report_position(&w, &ctl, i as usize);
// TRACES: FR-UI-8
// Written now rather than on a settle: which view you are in is
// the field a debounce is most likely to lose, and quitting
// straight from develop is exactly the case worth getting right.
write_place(&w, &ctl);
}
});
}
// TRACES: FR-UI-4
@@ -259,12 +263,12 @@ pub fn wire<F>(
let weak = window.as_weak();
let ctl = ctl.clone();
let on_open_image = on_open_image.clone();
window.on_library_roll_pick(move |i| {
window.global::<Library>().on_library_roll_pick(move |i| {
let Some(w) = weak.upgrade() else { return };
let Some(path) = ctl.paths.borrow().get(i as usize).cloned() else {
return;
};
w.set_library_roll_current(i);
w.global::<Library>().set_library_roll_current(i);
on_open_image(path);
report_position(&w, &ctl, i as usize);
// TRACES: FR-UI-8
@@ -307,10 +311,12 @@ fn wire_grid_cursor_and_zoom(
let weak = window.as_weak();
let ctl = ctl.clone();
let coll = coll_ctl.clone();
window.on_library_move_cursor(move |delta, extend| {
let Some(w) = weak.upgrade() else { return };
move_cursor(&w, &ctl, &coll, delta, extend);
});
window
.global::<Library>()
.on_library_move_cursor(move |delta, extend| {
let Some(w) = weak.upgrade() else { return };
move_cursor(&w, &ctl, &coll, delta, extend);
});
}
{
@@ -318,7 +324,7 @@ fn wire_grid_cursor_and_zoom(
let ctl = ctl.clone();
let coll = coll_ctl.clone();
let on_open = on_open_image.clone();
window.on_library_open_cursor(move || {
window.global::<Library>().on_library_open_cursor(move || {
let Some(w) = weak.upgrade() else { return };
// The cursor is a library ordinal and `paths` is the loaded
// window, so the row is the difference. A cursor outside the
@@ -333,8 +339,9 @@ fn wire_grid_cursor_and_zoom(
w.set_active_view(View::Develop);
// As on a click: the roll marks what is open, and centres on it
// because this too begins a session.
w.set_library_roll_current(row.unwrap_or(0) as i32);
w.set_library_roll_centre(true);
w.global::<Library>()
.set_library_roll_current(row.unwrap_or(0) as i32);
w.global::<Library>().set_library_roll_centre(true);
on_open(path);
report_position(&w, &ctl, row.unwrap_or(0));
write_place(&w, &ctl);
@@ -350,33 +357,35 @@ fn wire_grid_cursor_and_zoom(
{
let weak = window.as_weak();
let ctl = ctl.clone();
window.on_library_zoom_cells(move |delta| {
let Some(w) = weak.upgrade() else { return };
window
.global::<Library>()
.on_library_zoom_cells(move |delta| {
let Some(w) = weak.upgrade() else { return };
let current = w.get_library_cell_size();
let next = if delta > 0 {
current * 1.25
} else {
current / 1.25
}
.clamp(MIN_CELL_SIZE, MAX_CELL_SIZE);
let current = w.global::<Library>().get_library_cell_size();
let next = if delta > 0 {
current * 1.25
} else {
current / 1.25
}
.clamp(MIN_CELL_SIZE, MAX_CELL_SIZE);
if (next - current).abs() < 0.5 {
return;
}
w.set_library_cell_size(next);
if (next - current).abs() < 0.5 {
return;
}
w.global::<Library>().set_library_cell_size(next);
// No need to forget anything on a class change: the class is part
// of the request key, so cells that now want the large resolution
// simply miss and ask for it, while the 256px ones they already
// hold stay served.
//
// Deferred, like every other geometry change. This one was still
// reloading inline — a full catalog re-read and model rebuild per
// step, which is what a wheel spun through six steps paid six
// times over.
schedule_reload(&w, &ctl);
});
// No need to forget anything on a class change: the class is part
// of the request key, so cells that now want the large resolution
// simply miss and ask for it, while the 256px ones they already
// hold stay served.
//
// Deferred, like every other geometry change. This one was still
// reloading inline — a full catalog re-read and model rebuild per
// step, which is what a wheel spun through six steps paid six
// times over.
schedule_reload(&w, &ctl);
});
}
// TRACES: FR-UI-4
@@ -391,20 +400,22 @@ fn wire_grid_cursor_and_zoom(
{
let weak = window.as_weak();
let ctl = ctl.clone();
window.on_library_pinch_cells(move |ratio| {
let Some(w) = weak.upgrade() else { return };
if !(ratio.is_finite() && ratio > 0.0) {
return;
}
window
.global::<Library>()
.on_library_pinch_cells(move |ratio| {
let Some(w) = weak.upgrade() else { return };
if !(ratio.is_finite() && ratio > 0.0) {
return;
}
let current = w.get_library_cell_size();
let next = (current * ratio).clamp(MIN_CELL_SIZE, MAX_CELL_SIZE);
if (next - current).abs() < 0.5 {
return;
}
w.set_library_cell_size(next);
schedule_reload(&w, &ctl);
});
let current = w.global::<Library>().get_library_cell_size();
let next = (current * ratio).clamp(MIN_CELL_SIZE, MAX_CELL_SIZE);
if (next - current).abs() < 0.5 {
return;
}
w.global::<Library>().set_library_cell_size(next);
schedule_reload(&w, &ctl);
});
}
// TRACES: FR-UI-4
@@ -418,10 +429,12 @@ fn wire_grid_cursor_and_zoom(
let weak = window.as_weak();
let ctl = ctl.clone();
let coll = coll_ctl.clone();
window.on_library_pinch_started(move || {
let Some(w) = weak.upgrade() else { return };
crate::collections_ui::cancel_press(&w, &coll, &ctl.visible_ids());
});
window
.global::<Library>()
.on_library_pinch_started(move || {
let Some(w) = weak.upgrade() else { return };
crate::collections_ui::cancel_press(&w, &coll, &ctl.visible_ids());
});
}
}
@@ -462,25 +475,27 @@ fn wire_grid_sync_and_load(window: &AppWindow, ctl: &Rc<LibraryController>) {
{
let weak = window.as_weak();
let ctl = ctl.clone();
window.on_library_columns_changed(move || {
if let Some(w) = weak.upgrade() {
if !w.get_library_visible() {
return;
}
window
.global::<Library>()
.on_library_columns_changed(move || {
if let Some(w) = weak.upgrade() {
if !w.get_library_visible() {
return;
}
// Deferred, and re-anchored when it lands — see
// [`schedule_reload`]. A column change moves every cell in the
// grid, because a cell is drawn at its absolute place in the
// library and the row that resolves to is `index / columns`.
// The viewport does not move with them, so without the
// re-anchor the view is left pointing at rows the loaded window
// no longer covers and the grid draws nothing at all — the
// "gallery randomly goes blank until I scroll" report, whose
// triggers are a resize, the sidebar opening, a zoom step, or
// turning the tablet over.
schedule_reload(&w, &ctl);
}
});
// Deferred, and re-anchored when it lands — see
// [`schedule_reload`]. A column change moves every cell in the
// grid, because a cell is drawn at its absolute place in the
// library and the row that resolves to is `index / columns`.
// The viewport does not move with them, so without the
// re-anchor the view is left pointing at rows the loaded window
// no longer covers and the grid draws nothing at all — the
// "gallery randomly goes blank until I scroll" report, whose
// triggers are a resize, the sidebar opening, a zoom step, or
// turning the tablet over.
schedule_reload(&w, &ctl);
}
});
}
// The viewport changed size, so the window it can usefully hold changed
@@ -489,23 +504,25 @@ fn wire_grid_sync_and_load(window: &AppWindow, ctl: &Rc<LibraryController>) {
{
let weak = window.as_weak();
let ctl = ctl.clone();
window.on_library_viewport_cells(move |on_screen| {
let Some(w) = weak.upgrade() else { return };
if !w.get_library_visible() {
return;
}
let on_screen = (on_screen.max(0) as usize).max(1);
let window_size = (on_screen * SCREENFULS).max(MIN_WINDOW);
if window_size == *ctl.window.borrow() && on_screen == ctl.viewport_cells.get() {
return;
}
ctl.viewport_cells.set(on_screen);
*ctl.window.borrow_mut() = window_size;
// Coalesced with the column change that almost always accompanies
// it: resizing the cells alters both, and reloading once per report
// meant two full rebuilds per zoom step.
schedule_reload(&w, &ctl);
});
window
.global::<Library>()
.on_library_viewport_cells(move |on_screen| {
let Some(w) = weak.upgrade() else { return };
if !w.get_library_visible() {
return;
}
let on_screen = (on_screen.max(0) as usize).max(1);
let window_size = (on_screen * SCREENFULS).max(MIN_WINDOW);
if window_size == *ctl.window.borrow() && on_screen == ctl.viewport_cells.get() {
return;
}
ctl.viewport_cells.set(on_screen);
*ctl.window.borrow_mut() = window_size;
// Coalesced with the column change that almost always accompanies
// it: resizing the cells alters both, and reloading once per report
// meant two full rebuilds per zoom step.
schedule_reload(&w, &ctl);
});
}
// Scrolling moves the loaded window through the library.
@@ -515,118 +532,121 @@ fn wire_grid_sync_and_load(window: &AppWindow, ctl: &Rc<LibraryController>) {
{
let weak = window.as_weak();
let ctl = ctl.clone();
window.on_library_scrolled(move |first_visible| {
let Some(w) = weak.upgrade() else { return };
let first_visible = first_visible.max(0) as usize;
window
.global::<Library>()
.on_library_scrolled(move |first_visible| {
let Some(w) = weak.upgrade() else { return };
let first_visible = first_visible.max(0) as usize;
// **A report from a grid that is not on screen is not a scroll.**
//
// The grid is gated on an `if`, so leaving it tears the whole
// subtree down — and a Flickable being destroyed passes its viewport
// through zero on the way out, which arrives here indistinguishable
// from the user having flung the grid to the top. Everything below
// then ran on the way *into* develop: the loaded window was reset to
// offset zero, the model was rebuilt against the first rows of the
// library, and a thumbnail batch was issued for photographs nobody
// had asked to see. Those rebuilds landed while the grid was still
// being taken apart, which is what flashed the library over the
// develop view for the first few frames after a click — and on a
// remote library it also spent a burst of requests on the top of the
// catalog every single time an image was opened.
//
// **`library-visible`, not `show-library`, and the difference is the
// whole bug.** `show-library` says "the library rather than develop"
// and stays true while Settings, Import, People or the launch screen
// replaces the window — all four of which take the grid down just as
// opening an image does. So the teardown's scroll-to-zero passed this
// guard, `resume_at` was set to 0, and coming back from any of those
// four screens landed at the top of the library however deep in it the
// user had been. The property is computed once in `app.slint` beside
// the `if` it is spelled from, so the two cannot drift apart again.
//
// The guard used to cover only `resume_at`, for a narrower version
// of the same reason. It belongs over the whole handler.
if !w.get_library_visible() {
return;
}
// **A report from a grid that is not on screen is not a scroll.**
//
// The grid is gated on an `if`, so leaving it tears the whole
// subtree down — and a Flickable being destroyed passes its viewport
// through zero on the way out, which arrives here indistinguishable
// from the user having flung the grid to the top. Everything below
// then ran on the way *into* develop: the loaded window was reset to
// offset zero, the model was rebuilt against the first rows of the
// library, and a thumbnail batch was issued for photographs nobody
// had asked to see. Those rebuilds landed while the grid was still
// being taken apart, which is what flashed the library over the
// develop view for the first few frames after a click — and on a
// remote library it also spent a burst of requests on the top of the
// catalog every single time an image was opened.
//
// **`library-visible`, not `show-library`, and the difference is the
// whole bug.** `show-library` says "the library rather than develop"
// and stays true while Settings, Import, People or the launch screen
// replaces the window — all four of which take the grid down just as
// opening an image does. So the teardown's scroll-to-zero passed this
// guard, `resume_at` was set to 0, and coming back from any of those
// four screens landed at the top of the library however deep in it the
// user had been. The property is computed once in `app.slint` beside
// the `if` it is spelled from, so the two cannot drift apart again.
//
// The guard used to cover only `resume_at`, for a narrower version
// of the same reason. It belongs over the whole handler.
if !w.get_library_visible() {
return;
}
// Remember where the view is, so leaving for the develop view and
// coming back returns here. Latched on every event rather than read
// at departure: by the time the grid is hidden its scroll position
// is only in the Flickable, which is about to be destroyed.
ctl.resume_at.set(first_visible);
// Remember where the view is, so leaving for the develop view and
// coming back returns here. Latched on every event rather than read
// at departure: by the time the grid is hidden its scroll position
// is only in the Flickable, which is about to be destroyed.
ctl.resume_at.set(first_visible);
// TRACES: FR-UI-8
// And where the *next launch* will look for it. Debounced: a flick
// reports several of these per screenful.
note_place(&w, &ctl);
// TRACES: FR-UI-8
// And where the *next launch* will look for it. Debounced: a flick
// reports several of these per screenful.
note_place(&w, &ctl);
// The same position where a rebuilt grid will look for it.
//
// `scroll-to` is read by `seek()`, which runs on a `scroll-token`
// change and on `init` — so writing it here without bumping the
// token cannot move the grid that is on screen, and *is* what the
// next one reads when it is built. That is every route back to the
// grid at once: Settings, Import, People and the launch screen all
// tear the subtree down and rebuild it, and none of them goes
// through `on_back_to_library` to have the position replayed by
// hand. Without this they each rebuilt against whatever `scroll-to`
// was last *set* to — a stale scrub, or zero — and landed there.
w.set_library_scroll_to(first_visible as i32);
// The same position where a rebuilt grid will look for it.
//
// `scroll-to` is read by `seek()`, which runs on a `scroll-token`
// change and on `init` — so writing it here without bumping the
// token cannot move the grid that is on screen, and *is* what the
// next one reads when it is built. That is every route back to the
// grid at once: Settings, Import, People and the launch screen all
// tear the subtree down and rebuild it, and none of them goes
// through `on_back_to_library` to have the position replayed by
// hand. Without this they each rebuilt against whatever `scroll-to`
// was last *set* to — a stale scrub, or zero — and landed there.
w.global::<Library>()
.set_library_scroll_to(first_visible as i32);
// Move the timeline marker with the view. Scrolling the grid is a
// way of moving through time just as scrubbing is, and a marker
// that only ever moved on a scrub sat still while the photographs
// beside it advanced by months — the axis said "when you are" and
// was wrong the moment the user touched the wheel.
//
// This runs before the reload guard below, which fires only a few
// times per screenful; the marker has to follow every event or it
// would advance in visible jerks.
//
// Only the marker is moved, not the whole histogram: rebuilding
// the bars means a `GROUP BY strftime` aggregate over the library,
// far too much for every event of a flick. The bars do not change
// as the grid scrolls anyway — only where the marker sits on them.
//
// Re-running the scrub would be wrong for a second reason: it sets
// `scroll-to`, which would drive the grid from its own scroll.
{
let borrow = ctl.catalog.borrow();
if let Some(catalog) = borrow.as_ref() {
if let Some(when) = capture_time_at(&ctl, catalog, first_visible) {
*ctl.current_bucket.borrow_mut() = Some(when);
// Move the timeline marker with the view. Scrolling the grid is a
// way of moving through time just as scrubbing is, and a marker
// that only ever moved on a scrub sat still while the photographs
// beside it advanced by months — the axis said "when you are" and
// was wrong the moment the user touched the wheel.
//
// This runs before the reload guard below, which fires only a few
// times per screenful; the marker has to follow every event or it
// would advance in visible jerks.
//
// Only the marker is moved, not the whole histogram: rebuilding
// the bars means a `GROUP BY strftime` aggregate over the library,
// far too much for every event of a flick. The bars do not change
// as the grid scrolls anyway — only where the marker sits on them.
//
// Re-running the scrub would be wrong for a second reason: it sets
// `scroll-to`, which would drive the grid from its own scroll.
{
let borrow = ctl.catalog.borrow();
if let Some(catalog) = borrow.as_ref() {
if let Some(when) = capture_time_at(&ctl, catalog, first_visible) {
*ctl.current_bucket.borrow_mut() = Some(when);
let zoom = *ctl.timeline_zoom.borrow();
let centre = *ctl.timeline_centre.borrow();
if let Some(full) = catalog_span(catalog, &ctl) {
let (from, to) = zoomed_span(full, zoom, centre);
w.set_library_current_bucket(when as i32);
w.set_library_current_fraction(
((when - from) as f64 / (to - from).max(1) as f64).clamp(0.0, 1.0)
as f32,
);
w.set_library_timeline_anchored(true);
let zoom = *ctl.timeline_zoom.borrow();
let centre = *ctl.timeline_centre.borrow();
if let Some(full) = catalog_span(catalog, &ctl) {
let (from, to) = zoomed_span(full, zoom, centre);
w.set_library_current_bucket(when as i32);
w.set_library_current_fraction(
((when - from) as f64 / (to - from).max(1) as f64)
.clamp(0.0, 1.0) as f32,
);
w.set_library_timeline_anchored(true);
}
}
}
}
}
// Centre the window on the view, so scrolling either way has
// loaded rows ahead of it rather than only below.
let Some(offset) = window_move(
first_visible,
*ctl.offset.borrow(),
*ctl.window.borrow(),
ctl.viewport_cells.get(),
w.get_library_total().max(0) as usize,
) else {
return;
};
// Centre the window on the view, so scrolling either way has
// loaded rows ahead of it rather than only below.
let Some(offset) = window_move(
first_visible,
*ctl.offset.borrow(),
*ctl.window.borrow(),
ctl.viewport_cells.get(),
w.global::<Library>().get_library_total().max(0) as usize,
) else {
return;
};
*ctl.offset.borrow_mut() = offset;
load_window(&w, &ctl);
});
*ctl.offset.borrow_mut() = offset;
load_window(&w, &ctl);
});
}
}
+4 -2
View File
@@ -13,7 +13,7 @@ use dr_catalog::Catalog;
use slint::ComponentHandle;
use crate::library;
use crate::AppWindow;
use crate::{AppWindow, Library};
use super::controller::{stop, LibraryController};
@@ -499,7 +499,9 @@ fn refresh_local_count(window: &AppWindow, ctl: &Rc<LibraryController>) {
let Some(catalog) = borrow.as_ref() else {
return;
};
window.set_library_local_count(library::local_original_count(catalog).unwrap_or(0) as i32);
window
.global::<Library>()
.set_library_local_count(library::local_original_count(catalog).unwrap_or(0) as i32);
}
/// TRACES: FR-NC-6a
+5 -4
View File
@@ -18,7 +18,7 @@ use dr_types::FormatFilter;
use slint::ComponentHandle;
use crate::library::{self, ScanMessage};
use crate::{AppWindow, View};
use crate::{AppWindow, Library, View};
use super::controller::{stop, LibraryController};
use super::offline::refresh_offline;
@@ -677,11 +677,12 @@ pub(super) fn schedule_reload(window: &AppWindow, ctl: &Rc<LibraryController>) {
// change to `columns` moves every one of them and a viewport left
// where it was would be pointing at rows the window no longer covers.
let window_size = *ctl_cb.window.borrow();
let total = w.get_library_total().max(0) as usize;
let total = w.global::<Library>().get_library_total().max(0) as usize;
*ctl_cb.offset.borrow_mut() = window_start(anchor, window_size, total);
load_window(&w, &ctl_cb);
w.set_library_scroll_to(anchor as i32);
w.set_library_scroll_token(w.get_library_scroll_token() + 1);
w.global::<Library>().set_library_scroll_to(anchor as i32);
w.global::<Library>()
.set_library_scroll_token(w.global::<Library>().get_library_scroll_token() + 1);
});
// Replacing the slot drops the previous timer, which is what makes this
+5 -3
View File
@@ -15,7 +15,7 @@ use dr_catalog::Catalog;
use slint::{ComponentHandle, Model as _};
use crate::library;
use crate::{AppWindow, KeywordRow};
use crate::{AppWindow, KeywordRow, Library};
use super::controller::{stop, LibraryController};
use super::window::load_window;
@@ -40,7 +40,7 @@ pub fn sync_ratings(window: &AppWindow, catalog: &Catalog, ids: &[dr_types::Imag
}
};
let model = window.get_library_cells();
let model = window.global::<Library>().get_library_cells();
for (row, id) in ids.iter().enumerate() {
// Absent means unrated, which is a real state rather than missing data.
let j = found.get(id).copied().unwrap_or_default();
@@ -71,7 +71,9 @@ pub(super) fn refresh_rating_counts(window: &AppWindow, catalog: &Catalog) {
// Shown for the same reason the star counts are: a filter that silently
// empties the grid reads as broken, and this one will legitimately be zero
// on a library nothing has been downloaded from yet.
window.set_library_local_count(library::local_original_count(catalog).unwrap_or(0) as i32);
window
.global::<Library>()
.set_library_local_count(library::local_original_count(catalog).unwrap_or(0) as i32);
}
// --- keywords (FR-CAT-5, FR-CAT-6) ---------------------------------------
+31 -15
View File
@@ -14,7 +14,7 @@ use dr_catalog::Catalog;
use slint::ComponentHandle;
use crate::library;
use crate::{AppWindow, TimelineBar, View};
use crate::{AppWindow, Library, TimelineBar, View};
use super::controller::LibraryController;
use super::filter_bar::{push_people_chips, refilter};
@@ -484,8 +484,12 @@ fn scrub_to(window: &AppWindow, ctl: &Rc<LibraryController>, when: i64) {
// Move the viewport as well as the window. Cells are drawn at their
// absolute place in the library, so loading rows around image 15,000 while
// the viewport sits at row 0 shows an empty grid until the user scrolls.
window.set_library_scroll_to(position as i32);
window.set_library_scroll_token(window.get_library_scroll_token() + 1);
window
.global::<Library>()
.set_library_scroll_to(position as i32);
window
.global::<Library>()
.set_library_scroll_token(window.global::<Library>().get_library_scroll_token() + 1);
load_window(window, ctl);
}
@@ -506,7 +510,7 @@ fn scrub_to(window: &AppWindow, ctl: &Rc<LibraryController>, when: i64) {
/// delete — after removing one frame from a wall of twenty thousand, the next
/// one you want is the one that just moved into its place.
pub fn restore_position(window: &AppWindow, ctl: &Rc<LibraryController>) {
let total = window.get_library_total().max(0) as usize;
let total = window.global::<Library>().get_library_total().max(0) as usize;
if total == 0 {
return;
}
@@ -522,8 +526,12 @@ pub fn restore_position(window: &AppWindow, ctl: &Rc<LibraryController>) {
// `resume_at` is what the grid last reported as its first visible image,
// which is the thing the photographer is actually looking at.
let anchor = ctl.resume_at.get().min(total - 1);
window.set_library_scroll_to(anchor as i32);
window.set_library_scroll_token(window.get_library_scroll_token() + 1);
window
.global::<Library>()
.set_library_scroll_to(anchor as i32);
window
.global::<Library>()
.set_library_scroll_token(window.global::<Library>().get_library_scroll_token() + 1);
}
/// Format a bucket start for the histogram's hover label.
@@ -765,7 +773,7 @@ pub(crate) fn apply_place(
// The filter and the scope have both moved the window to the top; this is
// what puts it back, and it has to run after them for that reason.
let total = window.get_library_total().max(0) as usize;
let total = window.global::<Library>().get_library_total().max(0) as usize;
let resolved = resolved.filter(|(at, _)| *at < total);
match resolved {
Some((at, _)) => {
@@ -786,16 +794,18 @@ pub(crate) fn apply_place(
if let Some((at, Found::Photograph)) = resolved {
if let Some(open) = ctl.open_image.borrow().clone() {
window.set_active_view(View::Develop);
window.set_library_roll_centre(true);
window.global::<Library>().set_library_roll_centre(true);
let offset = *ctl.offset.borrow();
if let Some(row) = at.checked_sub(offset) {
window.set_library_roll_current(row as i32);
window
.global::<Library>()
.set_library_roll_current(row as i32);
}
open(place.path.clone());
// After the open, which resets the readout to "1 of 1" on its
// way in — the same ordering `report_position` documents.
window.set_index(at as i32);
window.set_total(window.get_library_total());
window.set_total(window.global::<Library>().get_library_total());
}
}
}
@@ -884,7 +894,9 @@ fn push_filter(window: &AppWindow, ctl: &Rc<LibraryController>) {
Some(dr_types::FlagState::Reject) => 2,
_ => 0,
});
window.set_library_local_only(filter.local_only);
window
.global::<Library>()
.set_library_local_only(filter.local_only);
ctl.local_only.set(filter.local_only);
window.set_library_filter_eyes_open(filter.eyes_open);
push_people_chips(window, ctl);
@@ -925,7 +937,7 @@ pub(super) fn resume_position(
coll: &Rc<crate::collections_ui::CollectionsController>,
focus: Option<usize>,
) {
let total = window.get_library_total().max(0) as usize;
let total = window.global::<Library>().get_library_total().max(0) as usize;
if total == 0 {
// Files named on the command line: there is no grid behind them, and
// an ordinal read off the readout would name a row of nothing.
@@ -955,12 +967,16 @@ pub(super) fn resume_position(
// Before the grid is shown, not after: the markup gates it on an `if`, and
// the rebuilt Flickable reads `scroll-to` in its `init`. Setting these
// afterwards would leave that init to run against the previous position.
window.set_library_scroll_to(anchor as i32);
window.set_library_scroll_token(window.get_library_scroll_token() + 1);
window
.global::<Library>()
.set_library_scroll_to(anchor as i32);
window
.global::<Library>()
.set_library_scroll_token(window.global::<Library>().get_library_scroll_token() + 1);
if let Some(at) = focus {
coll.set_cursor(Some(at));
window.set_library_cursor(at as i32);
window.global::<Library>().set_library_cursor(at as i32);
}
// The view has moved, so the capture-time marker has to move with it.
+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() {