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:
@@ -45,9 +45,9 @@ use dr_catalog::bursts::{self, Rules, Signature};
|
||||
use dr_catalog::Catalog;
|
||||
use dr_thumbs::{ThumbSize, ThumbStore};
|
||||
use dr_types::ImageId;
|
||||
use slint::Model as _;
|
||||
use slint::{ComponentHandle as _, Model as _};
|
||||
|
||||
use crate::AppWindow;
|
||||
use crate::{AppWindow, Library};
|
||||
|
||||
/// How many signatures are written per transaction.
|
||||
///
|
||||
@@ -364,7 +364,7 @@ pub fn sync_badges(window: &AppWindow, catalog: &Catalog, ids: &[ImageId]) {
|
||||
}
|
||||
};
|
||||
|
||||
let model = window.get_library_cells();
|
||||
let model = window.global::<Library>().get_library_cells();
|
||||
for (row, id) in ids.iter().enumerate() {
|
||||
let (count, expanded, representative) = match found.get(id) {
|
||||
Some(m) => (m.size as i32, m.expanded, m.representative),
|
||||
|
||||
@@ -10,7 +10,7 @@ use dr_catalog::Catalog;
|
||||
use dr_types::CollectionId;
|
||||
use slint::ComponentHandle;
|
||||
|
||||
use crate::AppWindow;
|
||||
use crate::{AppWindow, Library};
|
||||
|
||||
use super::controller::CollectionsController;
|
||||
use super::tree_sync::refresh_tree;
|
||||
@@ -189,11 +189,11 @@ pub(super) fn arm_hold(window: &AppWindow, ctl: &Rc<CollectionsController>, row:
|
||||
// This half happens whether or not selection mode was already on —
|
||||
// it is the half a *drag* needs, and a drag out of a selection of
|
||||
// forty starts in a mode that is already on.
|
||||
w.set_library_held_row(row);
|
||||
w.global::<Library>().set_library_held_row(row);
|
||||
|
||||
if !ctl_cb.select_mode.get() {
|
||||
ctl_cb.select_mode.set(true);
|
||||
w.set_library_select_mode(true);
|
||||
w.global::<Library>().set_library_select_mode(true);
|
||||
}
|
||||
|
||||
// The release that follows this hold must not also open the image:
|
||||
|
||||
@@ -6,8 +6,9 @@ use std::collections::BTreeSet;
|
||||
use std::rc::Rc;
|
||||
|
||||
use dr_types::ImageId;
|
||||
use slint::ComponentHandle as _;
|
||||
|
||||
use crate::AppWindow;
|
||||
use crate::{AppWindow, Library};
|
||||
|
||||
use super::controller::CollectionsController;
|
||||
use super::tree_sync::sync_selection;
|
||||
@@ -292,7 +293,7 @@ pub fn cancel_press(window: &AppWindow, ctl: &Rc<CollectionsController>, ids: &[
|
||||
// has to put it back down, or the ring stays open around a cell nobody is
|
||||
// touching and the grid stays frozen with it.
|
||||
ctl.pending_toggle.set(None);
|
||||
window.set_library_held_row(-1);
|
||||
window.global::<Library>().set_library_held_row(-1);
|
||||
|
||||
let Some(undo) = ctl.press_undo.borrow_mut().take() else {
|
||||
return;
|
||||
|
||||
@@ -13,7 +13,7 @@ use dr_catalog::Catalog;
|
||||
use dr_types::{CollectionId, ImageId};
|
||||
use slint::{ComponentHandle as _, Model as _};
|
||||
|
||||
use crate::{AppWindow, CollectionRow, Collections};
|
||||
use crate::{AppWindow, CollectionRow, Collections, Library};
|
||||
|
||||
use super::controller::CollectionsController;
|
||||
|
||||
@@ -195,7 +195,7 @@ fn offline_state(
|
||||
/// Separate from [`sync_selection`] because the two differ: the selection
|
||||
/// persists after the drag, the lift lasts only while it is in flight.
|
||||
pub fn sync_lifted(window: &AppWindow, lifted: &[ImageId], ids: &[ImageId]) {
|
||||
let model = window.get_library_cells();
|
||||
let model = window.global::<Library>().get_library_cells();
|
||||
for (row, id) in ids.iter().enumerate() {
|
||||
let want = lifted.contains(id);
|
||||
if let Some(mut cell) = model.row_data(row) {
|
||||
@@ -213,7 +213,7 @@ pub fn sync_lifted(window: &AppWindow, lifted: &[ImageId], ids: &[ImageId]) {
|
||||
/// this is what keeps the two in step after a scrub, a drop, or a click.
|
||||
pub fn sync_selection(window: &AppWindow, ctl: &Rc<CollectionsController>, ids: &[ImageId]) {
|
||||
let selection = ctl.selection.borrow();
|
||||
let model = window.get_library_cells();
|
||||
let model = window.global::<Library>().get_library_cells();
|
||||
|
||||
// The anchor is deliberately **not** pushed to the grid.
|
||||
//
|
||||
@@ -238,7 +238,9 @@ pub fn sync_selection(window: &AppWindow, ctl: &Rc<CollectionsController>, ids:
|
||||
}
|
||||
}
|
||||
|
||||
window.set_library_selected_count(selection.len() as i32);
|
||||
window
|
||||
.global::<Library>()
|
||||
.set_library_selected_count(selection.len() as i32);
|
||||
}
|
||||
|
||||
/// TRACES: FR-CAT-7
|
||||
@@ -261,7 +263,7 @@ pub(super) fn sync_reorderable(window: &AppWindow) {
|
||||
.filter_map(|i| rows.row_data(i))
|
||||
.find(|r| r.id == id)
|
||||
.is_some_and(|r| !r.smart && !r.has_children);
|
||||
window.set_library_reorderable(manual);
|
||||
window.global::<Library>().set_library_reorderable(manual);
|
||||
}
|
||||
|
||||
/// Refresh the per-cell "in this many collections" badges.
|
||||
@@ -300,7 +302,7 @@ pub fn sync_badges(window: &AppWindow, catalog: &Catalog, ids: &[ImageId]) {
|
||||
}
|
||||
}
|
||||
|
||||
let model = window.get_library_cells();
|
||||
let model = window.global::<Library>().get_library_cells();
|
||||
for (row, id) in ids.iter().enumerate() {
|
||||
let want = counts.get(&(id.0 as i64)).copied().unwrap_or(0);
|
||||
if let Some(mut cell) = model.row_data(row) {
|
||||
|
||||
@@ -16,7 +16,7 @@ use dr_types::{CollectionId, ImageId};
|
||||
use slint::{ComponentHandle, Model as _};
|
||||
|
||||
use crate::library;
|
||||
use crate::{AppWindow, Collections};
|
||||
use crate::{AppWindow, Collections, Library};
|
||||
|
||||
use super::controller::{decide_drop, CollectionsController, Drop};
|
||||
use super::drag::{arm_hold, arm_spring, collapse_spring_opened, compose_drag_image};
|
||||
@@ -99,35 +99,37 @@ fn wire_selection(
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
let visible = visible_ids.clone();
|
||||
window.on_library_cell_pressed(move |row, ctrl_held, shift_held| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let ids = visible();
|
||||
window
|
||||
.global::<Library>()
|
||||
.on_library_cell_pressed(move |row, ctrl_held, shift_held| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let ids = visible();
|
||||
|
||||
// Consulted by the click that follows: a modified press is building
|
||||
// a selection and must not also navigate to develop.
|
||||
ctl.modified_press.set(ctrl_held || shift_held);
|
||||
// Consulted by the click that follows: a modified press is building
|
||||
// a selection and must not also navigate to develop.
|
||||
ctl.modified_press.set(ctrl_held || shift_held);
|
||||
|
||||
// Where the window starts, so the press is recorded as the ordinal
|
||||
// it is rather than as a row that stops meaning this photograph on
|
||||
// the next scroll.
|
||||
let offset = w.get_library_offset().max(0) as usize;
|
||||
select_row(&w, &ctl, &ids, offset, row as usize, ctrl_held, shift_held);
|
||||
// Where the window starts, so the press is recorded as the ordinal
|
||||
// it is rather than as a row that stops meaning this photograph on
|
||||
// the next scroll.
|
||||
let offset = w.get_library_offset().max(0) as usize;
|
||||
select_row(&w, &ctl, &ids, offset, row as usize, ctrl_held, shift_held);
|
||||
|
||||
// TRACES: FR-UI-2 | FR-UI-4 | FR-CAT-7
|
||||
// And start counting, in case this press is a hold. The press has
|
||||
// already selected this one cell; what the hold adds is the *mode*,
|
||||
// so the taps that follow go on selecting instead of opening the
|
||||
// next photograph the user touches — and the *pick-up*, which is
|
||||
// what makes the drag reliable.
|
||||
//
|
||||
// Armed even when the mode is already on, which it did not used to
|
||||
// be: there was nothing left for the hold to switch on, so it was
|
||||
// skipped. But the mode being on is exactly the state a
|
||||
// multi-image drag starts from, and skipping the hold left that
|
||||
// drag with no pick-up and no cue — the one gesture that most
|
||||
// needed both. See `arm_hold`.
|
||||
arm_hold(&w, &ctl, row);
|
||||
});
|
||||
// TRACES: FR-UI-2 | FR-UI-4 | FR-CAT-7
|
||||
// And start counting, in case this press is a hold. The press has
|
||||
// already selected this one cell; what the hold adds is the *mode*,
|
||||
// so the taps that follow go on selecting instead of opening the
|
||||
// next photograph the user touches — and the *pick-up*, which is
|
||||
// what makes the drag reliable.
|
||||
//
|
||||
// Armed even when the mode is already on, which it did not used to
|
||||
// be: there was nothing left for the hold to switch on, so it was
|
||||
// skipped. But the mode being on is exactly the state a
|
||||
// multi-image drag starts from, and skipping the hold left that
|
||||
// drag with no pick-up and no cue — the one gesture that most
|
||||
// needed both. See `arm_hold`.
|
||||
arm_hold(&w, &ctl, row);
|
||||
});
|
||||
}
|
||||
|
||||
// TRACES: FR-UI-2 | FR-UI-4
|
||||
@@ -135,9 +137,11 @@ fn wire_selection(
|
||||
// travelled. Either way the hold is off.
|
||||
{
|
||||
let ctl = ctl.clone();
|
||||
window.on_library_cell_press_ended(move || {
|
||||
*ctl.hold_timer.borrow_mut() = None;
|
||||
});
|
||||
window
|
||||
.global::<Library>()
|
||||
.on_library_cell_press_ended(move || {
|
||||
*ctl.hold_timer.borrow_mut() = None;
|
||||
});
|
||||
}
|
||||
|
||||
// TRACES: FR-CAT-5
|
||||
@@ -152,11 +156,13 @@ fn wire_selection(
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
let visible = visible_ids.clone();
|
||||
window.on_library_clear_selection(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
ctl.clear_selection();
|
||||
sync_selection(&w, &ctl, &visible());
|
||||
});
|
||||
window
|
||||
.global::<Library>()
|
||||
.on_library_clear_selection(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
ctl.clear_selection();
|
||||
sync_selection(&w, &ctl, &visible());
|
||||
});
|
||||
}
|
||||
|
||||
// TRACES: FR-CAT-7
|
||||
@@ -179,75 +185,77 @@ fn wire_selection(
|
||||
let catalog = catalog.clone();
|
||||
let visible = visible_ids.clone();
|
||||
let reload = on_scope_changed.clone();
|
||||
window.on_library_reorder_to(move |row, after| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
// Both guards belong here rather than only in `library.slint`: the
|
||||
// scope can change between the drag starting and the drop landing.
|
||||
if !w.get_library_reorderable() {
|
||||
return;
|
||||
}
|
||||
let Some(scope) = *ctl.scope.borrow() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let moving = ctl.selected();
|
||||
if moving.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let ids = visible();
|
||||
let Some(&target) = usize::try_from(row).ok().and_then(|r| ids.get(r)) else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Dropped on one of its own. There is no gap between a run and
|
||||
// itself to land in, and rewriting the whole membership to say so
|
||||
// would be a revision bump for no change.
|
||||
if moving.contains(&target) {
|
||||
return;
|
||||
}
|
||||
|
||||
let borrow = catalog.borrow();
|
||||
let Some(cat) = borrow.as_ref() else { return };
|
||||
|
||||
let current = match library::read_member_order(cat, scope) {
|
||||
Ok(current) => current,
|
||||
Err(e) => {
|
||||
w.global::<Collections>().set_collection_error(
|
||||
format!("reading the collection's order: {e}").into(),
|
||||
);
|
||||
window
|
||||
.global::<Library>()
|
||||
.on_library_reorder_to(move |row, after| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
// Both guards belong here rather than only in `library.slint`: the
|
||||
// scope can change between the drag starting and the drop landing.
|
||||
if !w.global::<Library>().get_library_reorderable() {
|
||||
return;
|
||||
}
|
||||
};
|
||||
let wanted = library::reordered(¤t, &moving, target, after);
|
||||
let Some(scope) = *ctl.scope.borrow() else {
|
||||
return;
|
||||
};
|
||||
|
||||
match coll::set_order(cat.connection(), scope, &wanted) {
|
||||
Ok(()) => {
|
||||
w.global::<Collections>()
|
||||
.set_collection_error(slint::SharedString::new());
|
||||
w.set_library_status(
|
||||
format!(
|
||||
"{} photograph{} moved",
|
||||
moving.len(),
|
||||
if moving.len() == 1 { "" } else { "s" }
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
drop(borrow);
|
||||
let moving = ctl.selected();
|
||||
if moving.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// The selection survives. It is what was just moved, and
|
||||
// dropping it would make a second nudge — which is how a
|
||||
// drag-to-reorder is usually corrected — start over.
|
||||
reload();
|
||||
sync_selection(&w, &ctl, &visible());
|
||||
let ids = visible();
|
||||
let Some(&target) = usize::try_from(row).ok().and_then(|r| ids.get(r)) else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Dropped on one of its own. There is no gap between a run and
|
||||
// itself to land in, and rewriting the whole membership to say so
|
||||
// would be a revision bump for no change.
|
||||
if moving.contains(&target) {
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
drop(borrow);
|
||||
w.global::<Collections>()
|
||||
.set_collection_error(format!("reordering: {e}").into());
|
||||
|
||||
let borrow = catalog.borrow();
|
||||
let Some(cat) = borrow.as_ref() else { return };
|
||||
|
||||
let current = match library::read_member_order(cat, scope) {
|
||||
Ok(current) => current,
|
||||
Err(e) => {
|
||||
w.global::<Collections>().set_collection_error(
|
||||
format!("reading the collection's order: {e}").into(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let wanted = library::reordered(¤t, &moving, target, after);
|
||||
|
||||
match coll::set_order(cat.connection(), scope, &wanted) {
|
||||
Ok(()) => {
|
||||
w.global::<Collections>()
|
||||
.set_collection_error(slint::SharedString::new());
|
||||
w.set_library_status(
|
||||
format!(
|
||||
"{} photograph{} moved",
|
||||
moving.len(),
|
||||
if moving.len() == 1 { "" } else { "s" }
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
drop(borrow);
|
||||
|
||||
// The selection survives. It is what was just moved, and
|
||||
// dropping it would make a second nudge — which is how a
|
||||
// drag-to-reorder is usually corrected — start over.
|
||||
reload();
|
||||
sync_selection(&w, &ctl, &visible());
|
||||
}
|
||||
Err(e) => {
|
||||
drop(borrow);
|
||||
w.global::<Collections>()
|
||||
.set_collection_error(format!("reordering: {e}").into());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// TRACES: FR-CAT-5 | FR-UI-4
|
||||
@@ -267,10 +275,10 @@ fn wire_selection(
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
let visible = visible_ids.clone();
|
||||
window.on_library_select_all(move || {
|
||||
window.global::<Library>().on_library_select_all(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let ids = visible();
|
||||
let total = w.get_library_total().max(0) as usize;
|
||||
let total = w.global::<Library>().get_library_total().max(0) as usize;
|
||||
if total == 0 {
|
||||
return;
|
||||
}
|
||||
@@ -334,69 +342,71 @@ fn wire_selection_filing(
|
||||
let catalog = catalog.clone();
|
||||
let visible = visible_ids.clone();
|
||||
let reload = on_scope_changed.clone();
|
||||
window.on_library_collection_from_selection(move |name| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let images = ctl.selected();
|
||||
if images.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let borrow = catalog.borrow();
|
||||
let Some(cat) = borrow.as_ref() else {
|
||||
w.global::<Collections>()
|
||||
.set_collection_error("Open a library first.".into());
|
||||
return;
|
||||
};
|
||||
|
||||
// The sheet refuses an empty name in two places, so this is
|
||||
// belt-and-braces rather than a path the UI can reach — but a
|
||||
// collection with no name at all is unfindable in the tree, and
|
||||
// falling back to the placeholder is recoverable where an empty
|
||||
// row is not.
|
||||
let name = match name.trim() {
|
||||
"" => unique_name(cat.connection(), None),
|
||||
typed => typed.to_string(),
|
||||
};
|
||||
|
||||
// The id is not carried out. It used to be, to open the rename
|
||||
// field on the row it names; now the name is already right, and
|
||||
// `refresh_tree` redraws the tree from the catalog either way.
|
||||
let made = coll::create(cat.connection(), &name, None, CollectionKind::Manual)
|
||||
.and_then(|id| coll::add_images(cat.connection(), id, &images));
|
||||
|
||||
match made {
|
||||
Ok(added) => {
|
||||
w.global::<Collections>()
|
||||
.set_collection_error(slint::SharedString::new());
|
||||
w.set_library_status(
|
||||
format!(
|
||||
"{added} photograph{} in {name}",
|
||||
if added == 1 { "" } else { "s" }
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
refresh_tree(&w, &ctl, cat);
|
||||
drop(borrow);
|
||||
|
||||
// The selection has been filed; holding on to it invites
|
||||
// the next press acting on photographs the user considers
|
||||
// dealt with.
|
||||
ctl.clear_selection();
|
||||
sync_selection(&w, &ctl, &visible());
|
||||
|
||||
// No rename opened here. The name was chosen before the
|
||||
// collection existed, so there is nothing left to correct —
|
||||
// and opening the sidebar's field is what stranded the
|
||||
// keyboard on a tablet.
|
||||
reload();
|
||||
window
|
||||
.global::<Library>()
|
||||
.on_library_collection_from_selection(move |name| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let images = ctl.selected();
|
||||
if images.is_empty() {
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
drop(borrow);
|
||||
|
||||
let borrow = catalog.borrow();
|
||||
let Some(cat) = borrow.as_ref() else {
|
||||
w.global::<Collections>()
|
||||
.set_collection_error(format!("making a collection: {e}").into());
|
||||
.set_collection_error("Open a library first.".into());
|
||||
return;
|
||||
};
|
||||
|
||||
// The sheet refuses an empty name in two places, so this is
|
||||
// belt-and-braces rather than a path the UI can reach — but a
|
||||
// collection with no name at all is unfindable in the tree, and
|
||||
// falling back to the placeholder is recoverable where an empty
|
||||
// row is not.
|
||||
let name = match name.trim() {
|
||||
"" => unique_name(cat.connection(), None),
|
||||
typed => typed.to_string(),
|
||||
};
|
||||
|
||||
// The id is not carried out. It used to be, to open the rename
|
||||
// field on the row it names; now the name is already right, and
|
||||
// `refresh_tree` redraws the tree from the catalog either way.
|
||||
let made = coll::create(cat.connection(), &name, None, CollectionKind::Manual)
|
||||
.and_then(|id| coll::add_images(cat.connection(), id, &images));
|
||||
|
||||
match made {
|
||||
Ok(added) => {
|
||||
w.global::<Collections>()
|
||||
.set_collection_error(slint::SharedString::new());
|
||||
w.set_library_status(
|
||||
format!(
|
||||
"{added} photograph{} in {name}",
|
||||
if added == 1 { "" } else { "s" }
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
refresh_tree(&w, &ctl, cat);
|
||||
drop(borrow);
|
||||
|
||||
// The selection has been filed; holding on to it invites
|
||||
// the next press acting on photographs the user considers
|
||||
// dealt with.
|
||||
ctl.clear_selection();
|
||||
sync_selection(&w, &ctl, &visible());
|
||||
|
||||
// No rename opened here. The name was chosen before the
|
||||
// collection existed, so there is nothing left to correct —
|
||||
// and opening the sidebar's field is what stranded the
|
||||
// keyboard on a tablet.
|
||||
reload();
|
||||
}
|
||||
Err(e) => {
|
||||
drop(borrow);
|
||||
w.global::<Collections>()
|
||||
.set_collection_error(format!("making a collection: {e}").into());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// TRACES: FR-UI-2 | FR-UI-4
|
||||
@@ -407,23 +417,25 @@ fn wire_selection_filing(
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
let visible = visible_ids.clone();
|
||||
window.on_library_toggle_select_mode(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let on = !ctl.select_mode.get();
|
||||
ctl.select_mode.set(on);
|
||||
w.set_library_select_mode(on);
|
||||
*ctl.hold_timer.borrow_mut() = None;
|
||||
window
|
||||
.global::<Library>()
|
||||
.on_library_toggle_select_mode(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let on = !ctl.select_mode.get();
|
||||
ctl.select_mode.set(on);
|
||||
w.global::<Library>().set_library_select_mode(on);
|
||||
*ctl.hold_timer.borrow_mut() = None;
|
||||
|
||||
// Leaving the mode drops the selection. "Done" reads as finishing
|
||||
// with these photographs, and a selection that outlived the mode
|
||||
// would still be acted on by the buttons in the header — which is
|
||||
// how forty images get exported by a user who thought they had put
|
||||
// them down.
|
||||
if !on {
|
||||
ctl.clear_selection();
|
||||
sync_selection(&w, &ctl, &visible());
|
||||
}
|
||||
});
|
||||
// Leaving the mode drops the selection. "Done" reads as finishing
|
||||
// with these photographs, and a selection that outlived the mode
|
||||
// would still be acted on by the buttons in the header — which is
|
||||
// how forty images get exported by a user who thought they had put
|
||||
// them down.
|
||||
if !on {
|
||||
ctl.clear_selection();
|
||||
sync_selection(&w, &ctl, &visible());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// TRACES: FR-CAT-7 | FR-UI-4
|
||||
@@ -434,59 +446,61 @@ fn wire_selection_filing(
|
||||
let catalog = catalog.clone();
|
||||
let visible = visible_ids.clone();
|
||||
let reload = on_scope_changed.clone();
|
||||
window.on_library_file_in_collection(move |id, moves| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let target = CollectionId(id as u64);
|
||||
let chosen = ctl.selected();
|
||||
if chosen.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let borrow = catalog.borrow();
|
||||
let Some(cat) = borrow.as_ref() else { return };
|
||||
|
||||
// Moving is only a move when there is somewhere to move *from*.
|
||||
// Unscoped, the sheet does not offer it; this guards the callback
|
||||
// anyway, and degrades to filing rather than refusing.
|
||||
let from = if moves { *ctl.scope.borrow() } else { None };
|
||||
let result = match from {
|
||||
Some(from) => coll::move_images(cat.connection(), from, target, &chosen),
|
||||
None => coll::add_images(cat.connection(), target, &chosen),
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(filed) => {
|
||||
w.global::<Collections>()
|
||||
.set_collection_error(slint::SharedString::new());
|
||||
// The honest count, as the drop reports it: claiming all
|
||||
// twelve when nine were already there teaches the user to
|
||||
// distrust every number the app shows them.
|
||||
let msg = if from.is_some() {
|
||||
format!("Moved {filed} of {}", chosen.len())
|
||||
} else if filed == chosen.len() {
|
||||
format!("Added {filed} to collection")
|
||||
} else {
|
||||
format!(
|
||||
"Added {filed} of {} — the rest were already there",
|
||||
chosen.len()
|
||||
)
|
||||
};
|
||||
w.set_library_status(msg.into());
|
||||
|
||||
refresh_tree(&w, &ctl, cat);
|
||||
sync_badges(&w, cat, &visible());
|
||||
// A move changes what the collection on screen holds, so
|
||||
// the grid has to be reread; filing elsewhere does not, but
|
||||
// the badge count on every cell just changed and the reread
|
||||
// is one query.
|
||||
drop(borrow);
|
||||
reload();
|
||||
window
|
||||
.global::<Library>()
|
||||
.on_library_file_in_collection(move |id, moves| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let target = CollectionId(id as u64);
|
||||
let chosen = ctl.selected();
|
||||
if chosen.is_empty() {
|
||||
return;
|
||||
}
|
||||
Err(e) => w
|
||||
.global::<Collections>()
|
||||
.set_collection_error(format!("filing: {e}").into()),
|
||||
}
|
||||
});
|
||||
|
||||
let borrow = catalog.borrow();
|
||||
let Some(cat) = borrow.as_ref() else { return };
|
||||
|
||||
// Moving is only a move when there is somewhere to move *from*.
|
||||
// Unscoped, the sheet does not offer it; this guards the callback
|
||||
// anyway, and degrades to filing rather than refusing.
|
||||
let from = if moves { *ctl.scope.borrow() } else { None };
|
||||
let result = match from {
|
||||
Some(from) => coll::move_images(cat.connection(), from, target, &chosen),
|
||||
None => coll::add_images(cat.connection(), target, &chosen),
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(filed) => {
|
||||
w.global::<Collections>()
|
||||
.set_collection_error(slint::SharedString::new());
|
||||
// The honest count, as the drop reports it: claiming all
|
||||
// twelve when nine were already there teaches the user to
|
||||
// distrust every number the app shows them.
|
||||
let msg = if from.is_some() {
|
||||
format!("Moved {filed} of {}", chosen.len())
|
||||
} else if filed == chosen.len() {
|
||||
format!("Added {filed} to collection")
|
||||
} else {
|
||||
format!(
|
||||
"Added {filed} of {} — the rest were already there",
|
||||
chosen.len()
|
||||
)
|
||||
};
|
||||
w.set_library_status(msg.into());
|
||||
|
||||
refresh_tree(&w, &ctl, cat);
|
||||
sync_badges(&w, cat, &visible());
|
||||
// A move changes what the collection on screen holds, so
|
||||
// the grid has to be reread; filing elsewhere does not, but
|
||||
// the badge count on every cell just changed and the reread
|
||||
// is one query.
|
||||
drop(borrow);
|
||||
reload();
|
||||
}
|
||||
Err(e) => w
|
||||
.global::<Collections>()
|
||||
.set_collection_error(format!("filing: {e}").into()),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -526,7 +540,7 @@ fn wire_drag(
|
||||
// nothing outside the application can interpret it as a paste.
|
||||
{
|
||||
let ctl = ctl.clone();
|
||||
window.on_library_drag_payload(move || {
|
||||
window.global::<Library>().on_library_drag_payload(move || {
|
||||
let carried = ctl.dragging.borrow().clone();
|
||||
let mut data = slint::DataTransfer::default();
|
||||
data.set_user_data(Rc::new(carried));
|
||||
@@ -551,67 +565,70 @@ fn wire_drag(
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
let visible = visible_ids.clone();
|
||||
window.on_library_drag_started(move |row| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let ids = visible();
|
||||
window
|
||||
.global::<Library>()
|
||||
.on_library_drag_started(move |row| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let ids = visible();
|
||||
|
||||
// TRACES: FR-UI-4
|
||||
// A drag is not a hold, and the timer must not outlive the press
|
||||
// that armed it. Grabbing a cell and moving inside 450 ms left the
|
||||
// hold armed underneath the drag, so it fired mid-gesture and put
|
||||
// the grid into selection mode the user had not asked for — the
|
||||
// drag finished into a mode that changed what every later tap
|
||||
// meant. The pinch already cancels for exactly this reason.
|
||||
*ctl.hold_timer.borrow_mut() = None;
|
||||
// TRACES: FR-UI-4
|
||||
// A drag is not a hold, and the timer must not outlive the press
|
||||
// that armed it. Grabbing a cell and moving inside 450 ms left the
|
||||
// hold armed underneath the drag, so it fired mid-gesture and put
|
||||
// the grid into selection mode the user had not asked for — the
|
||||
// drag finished into a mode that changed what every later tap
|
||||
// meant. The pinch already cancels for exactly this reason.
|
||||
*ctl.hold_timer.borrow_mut() = None;
|
||||
|
||||
// TRACES: FR-CAT-7
|
||||
// And this press was not a tap, so it never gets to take anything
|
||||
// out of the selection — see [`Press::Deferred`]. Dropped here as
|
||||
// well as on the next press because the drag reads the selection
|
||||
// one line below, and a removal still pending would be a
|
||||
// photograph the user can see is selected and the drop would not
|
||||
// carry.
|
||||
ctl.pending_toggle.set(None);
|
||||
// TRACES: FR-CAT-7
|
||||
// And this press was not a tap, so it never gets to take anything
|
||||
// out of the selection — see [`Press::Deferred`]. Dropped here as
|
||||
// well as on the next press because the drag reads the selection
|
||||
// one line below, and a removal still pending would be a
|
||||
// photograph the user can see is selected and the drop would not
|
||||
// carry.
|
||||
ctl.pending_toggle.set(None);
|
||||
|
||||
// Dragging an *unselected* cell carries only that one, and makes it
|
||||
// the selection — otherwise the images that travel are not the ones
|
||||
// the user grabbed. Dragging a selected cell carries the whole
|
||||
// selection, which is the multi-image gesture.
|
||||
let carried: Vec<ImageId> = {
|
||||
let mut selection = ctl.selection.borrow_mut();
|
||||
match ids.get(row as usize) {
|
||||
Some(id) if !selection.contains(id) => {
|
||||
selection.clear();
|
||||
selection.insert(*id);
|
||||
vec![*id]
|
||||
// Dragging an *unselected* cell carries only that one, and makes it
|
||||
// the selection — otherwise the images that travel are not the ones
|
||||
// the user grabbed. Dragging a selected cell carries the whole
|
||||
// selection, which is the multi-image gesture.
|
||||
let carried: Vec<ImageId> = {
|
||||
let mut selection = ctl.selection.borrow_mut();
|
||||
match ids.get(row as usize) {
|
||||
Some(id) if !selection.contains(id) => {
|
||||
selection.clear();
|
||||
selection.insert(*id);
|
||||
vec![*id]
|
||||
}
|
||||
_ => selection.iter().copied().collect(),
|
||||
}
|
||||
_ => selection.iter().copied().collect(),
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// The bitmap under the cursor, built from the thumbnails already in
|
||||
// the model — the drag carries what the user can see, and a cell
|
||||
// whose preview has not landed yet contributes nothing rather than
|
||||
// a placeholder.
|
||||
let thumbs: Vec<slint::Image> = {
|
||||
let model = w.get_library_cells();
|
||||
carried
|
||||
.iter()
|
||||
.filter_map(|id| ids.iter().position(|v| v == id))
|
||||
.filter_map(|row| model.row_data(row))
|
||||
.filter(|c| c.has_thumb)
|
||||
.map(|c| c.thumbnail)
|
||||
.collect()
|
||||
};
|
||||
w.set_library_drag_image(compose_drag_image(&thumbs));
|
||||
// The bitmap under the cursor, built from the thumbnails already in
|
||||
// the model — the drag carries what the user can see, and a cell
|
||||
// whose preview has not landed yet contributes nothing rather than
|
||||
// a placeholder.
|
||||
let thumbs: Vec<slint::Image> = {
|
||||
let model = w.global::<Library>().get_library_cells();
|
||||
carried
|
||||
.iter()
|
||||
.filter_map(|id| ids.iter().position(|v| v == id))
|
||||
.filter_map(|row| model.row_data(row))
|
||||
.filter(|c| c.has_thumb)
|
||||
.map(|c| c.thumbnail)
|
||||
.collect()
|
||||
};
|
||||
w.global::<Library>()
|
||||
.set_library_drag_image(compose_drag_image(&thumbs));
|
||||
|
||||
*ctl.dragging.borrow_mut() = carried.clone();
|
||||
sync_selection(&w, &ctl, &ids);
|
||||
// The lift-out: these cells fade and shrink in place, so the grid
|
||||
// shows where the photographs came from while the cursor shows them
|
||||
// in full colour.
|
||||
sync_lifted(&w, &carried, &ids);
|
||||
});
|
||||
*ctl.dragging.borrow_mut() = carried.clone();
|
||||
sync_selection(&w, &ctl, &ids);
|
||||
// The lift-out: these cells fade and shrink in place, so the grid
|
||||
// shows where the photographs came from while the cursor shows them
|
||||
// in full colour.
|
||||
sync_lifted(&w, &carried, &ids);
|
||||
});
|
||||
}
|
||||
|
||||
// A drag dwelling over a collapsed collection springs it open, so a nested
|
||||
@@ -740,52 +757,55 @@ fn wire_drag(
|
||||
let visible = visible_ids.clone();
|
||||
let reload = on_scope_changed.clone();
|
||||
let session = session.clone();
|
||||
window.on_library_drag_finished(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
window
|
||||
.global::<Library>()
|
||||
.on_library_drag_finished(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
|
||||
let landed = ctl.dropped_on.borrow_mut().take();
|
||||
let to_trash = ctl.trash_requested.borrow_mut().take();
|
||||
ctl.dragging.borrow_mut().clear();
|
||||
// The grid clears this itself on the cancel that starts a drag;
|
||||
// this is for the endings that reach no cell — a drop, or a drag
|
||||
// abandoned over nothing.
|
||||
w.set_library_held_row(-1);
|
||||
*ctl.hover_id.borrow_mut() = None;
|
||||
*ctl.spring_timer.borrow_mut() = None;
|
||||
let landed = ctl.dropped_on.borrow_mut().take();
|
||||
let to_trash = ctl.trash_requested.borrow_mut().take();
|
||||
ctl.dragging.borrow_mut().clear();
|
||||
// The grid clears this itself on the cancel that starts a drag;
|
||||
// this is for the endings that reach no cell — a drop, or a drag
|
||||
// abandoned over nothing.
|
||||
w.global::<Library>().set_library_held_row(-1);
|
||||
*ctl.hover_id.borrow_mut() = None;
|
||||
*ctl.spring_timer.borrow_mut() = None;
|
||||
|
||||
// A soft delete, deferred out of the drop handler so the models it
|
||||
// replaces are no longer being walked.
|
||||
if let Some(images) = to_trash {
|
||||
start_trash(&w, &ctl, &catalog, &session, &images, &reload);
|
||||
}
|
||||
|
||||
// The cells settle back into the grid, and the cursor bitmap is
|
||||
// released — it holds a copy of every thumbnail it composited.
|
||||
sync_lifted(&w, &[], &visible());
|
||||
w.set_library_drag_image(slint::Image::default());
|
||||
|
||||
if landed.is_some() {
|
||||
let borrow = catalog.borrow();
|
||||
if let Some(cat) = borrow.as_ref() {
|
||||
// Counts changed on the target and every ancestor, and the
|
||||
// dropped images now carry one more collection badge.
|
||||
refresh_tree(&w, &ctl, cat);
|
||||
sync_badges(&w, cat, &visible());
|
||||
// A soft delete, deferred out of the drop handler so the models it
|
||||
// replaces are no longer being walked.
|
||||
if let Some(images) = to_trash {
|
||||
start_trash(&w, &ctl, &catalog, &session, &images, &reload);
|
||||
}
|
||||
}
|
||||
|
||||
// Put back whatever the spring opened on the way. The collection
|
||||
// that received the images — and its ancestors — stay open, since
|
||||
// that is where the user is now working; on an abandoned drag
|
||||
// `landed` is `None` and everything closes.
|
||||
collapse_spring_opened(&w, &ctl, &catalog, landed);
|
||||
// The cells settle back into the grid, and the cursor bitmap is
|
||||
// released — it holds a copy of every thumbnail it composited.
|
||||
sync_lifted(&w, &[], &visible());
|
||||
w.global::<Library>()
|
||||
.set_library_drag_image(slint::Image::default());
|
||||
|
||||
// A drop into the collection currently being shown changes what that
|
||||
// collection holds, so the grid has to be reread.
|
||||
if landed.is_some() && landed == *ctl.scope.borrow() {
|
||||
reload();
|
||||
}
|
||||
});
|
||||
if landed.is_some() {
|
||||
let borrow = catalog.borrow();
|
||||
if let Some(cat) = borrow.as_ref() {
|
||||
// Counts changed on the target and every ancestor, and the
|
||||
// dropped images now carry one more collection badge.
|
||||
refresh_tree(&w, &ctl, cat);
|
||||
sync_badges(&w, cat, &visible());
|
||||
}
|
||||
}
|
||||
|
||||
// Put back whatever the spring opened on the way. The collection
|
||||
// that received the images — and its ancestors — stay open, since
|
||||
// that is where the user is now working; on an abandoned drag
|
||||
// `landed` is `None` and everything closes.
|
||||
collapse_spring_opened(&w, &ctl, &catalog, landed);
|
||||
|
||||
// A drop into the collection currently being shown changes what that
|
||||
// collection holds, so the grid has to be reread.
|
||||
if landed.is_some() && landed == *ctl.scope.borrow() {
|
||||
reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ use dr_catalog::Catalog;
|
||||
use dr_types::{CollectionId, ImageId};
|
||||
use slint::{ComponentHandle, Model as _};
|
||||
|
||||
use crate::{AppWindow, Collections};
|
||||
use crate::{AppWindow, Collections, Library};
|
||||
|
||||
use super::controller::{
|
||||
decide_delete, decide_release, CollectionsController, DeleteStep, Release,
|
||||
@@ -236,41 +236,44 @@ pub(super) fn wire_remove(
|
||||
let catalog = catalog.clone();
|
||||
let visible = visible_ids.clone();
|
||||
let reload = on_scope_changed.clone();
|
||||
window.on_library_remove_from_collection(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let Some(scope) = *ctl.scope.borrow() else {
|
||||
// Unscoped, there is no collection to remove from. The button
|
||||
// is hidden in that state; this guards the callback anyway.
|
||||
return;
|
||||
};
|
||||
let chosen = ctl.selected();
|
||||
if chosen.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let borrow = catalog.borrow();
|
||||
let Some(cat) = borrow.as_ref() else { return };
|
||||
|
||||
match coll::remove_images(cat.connection(), scope, &chosen) {
|
||||
Ok(n) => {
|
||||
w.global::<Collections>()
|
||||
.set_collection_error(slint::SharedString::new());
|
||||
// Named explicitly as a membership change: the images are
|
||||
// still in the library, and a user who reads this as a
|
||||
// delete will not trust the feature again.
|
||||
w.set_library_status(
|
||||
format!("Removed {n} from this collection; still in the library").into(),
|
||||
);
|
||||
ctl.clear_selection();
|
||||
refresh_tree(&w, &ctl, cat);
|
||||
sync_badges(&w, cat, &visible());
|
||||
reload();
|
||||
window
|
||||
.global::<Library>()
|
||||
.on_library_remove_from_collection(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let Some(scope) = *ctl.scope.borrow() else {
|
||||
// Unscoped, there is no collection to remove from. The button
|
||||
// is hidden in that state; this guards the callback anyway.
|
||||
return;
|
||||
};
|
||||
let chosen = ctl.selected();
|
||||
if chosen.is_empty() {
|
||||
return;
|
||||
}
|
||||
Err(e) => w
|
||||
.global::<Collections>()
|
||||
.set_collection_error(format!("removing: {e}").into()),
|
||||
}
|
||||
});
|
||||
|
||||
let borrow = catalog.borrow();
|
||||
let Some(cat) = borrow.as_ref() else { return };
|
||||
|
||||
match coll::remove_images(cat.connection(), scope, &chosen) {
|
||||
Ok(n) => {
|
||||
w.global::<Collections>()
|
||||
.set_collection_error(slint::SharedString::new());
|
||||
// Named explicitly as a membership change: the images are
|
||||
// still in the library, and a user who reads this as a
|
||||
// delete will not trust the feature again.
|
||||
w.set_library_status(
|
||||
format!("Removed {n} from this collection; still in the library")
|
||||
.into(),
|
||||
);
|
||||
ctl.clear_selection();
|
||||
refresh_tree(&w, &ctl, cat);
|
||||
sync_badges(&w, cat, &visible());
|
||||
reload();
|
||||
}
|
||||
Err(e) => w
|
||||
.global::<Collections>()
|
||||
.set_collection_error(format!("removing: {e}").into()),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -688,13 +691,15 @@ pub(super) fn wire_membership(
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
let catalog = catalog.clone();
|
||||
window.on_library_open_membership(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let borrow = catalog.borrow();
|
||||
let Some(cat) = borrow.as_ref() else { return };
|
||||
refresh_membership(&w, &ctl, cat);
|
||||
w.global::<Collections>().set_membership_open(true);
|
||||
});
|
||||
window
|
||||
.global::<Library>()
|
||||
.on_library_open_membership(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let borrow = catalog.borrow();
|
||||
let Some(cat) = borrow.as_ref() else { return };
|
||||
refresh_membership(&w, &ctl, cat);
|
||||
w.global::<Collections>().set_membership_open(true);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
+2
-1
@@ -72,6 +72,7 @@ use std::rc::Rc;
|
||||
|
||||
use anyhow::Result;
|
||||
use dr_decode::{Metadata, PreviewSize};
|
||||
use slint::ComponentHandle as _;
|
||||
|
||||
pub use develop::DevelopSession;
|
||||
|
||||
@@ -3436,7 +3437,7 @@ fn back_one_step(w: &AppWindow) -> bool {
|
||||
// Files named on the command line have no grid behind them — the same
|
||||
// condition the status strip uses to decide whether to offer the way
|
||||
// back at all.
|
||||
has_grid: w.get_library_total() > 0,
|
||||
has_grid: w.global::<Library>().get_library_total() > 0,
|
||||
scoped: w.get_collection_selected() != 0,
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) ---------------------------------------
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user