Give the collections sidebar its own Slint global

AppWindow carried the sidebar's tree, its row menu, renaming, drag and
drop between rows, the trash row, and the membership sheet as ~40
properties and callbacks on the root component, in the pattern CH-1
describes and the develop screen's globals (Adjustments, Framing, Steps,
...) already replaced. Collections.* in collections.slint now holds that
state, declared next to the MembershipRow struct it and the membership
sheet both use; Rust reaches it through window.global::<Collections>()
instead of window.set_/get_/on_/invoke_ on the root.

collection-selected, collection-select, and collection-offline-menu stay
on AppWindow: library_ui.rs invokes collection-select directly and
registers collection-offline-menu's handler, and lib.rs reads
collection-selected for back-navigation, so moving them would have meant
editing library_ui.rs, which another change on this branch is splitting
into a module directory. collections-visible stays too — it is
lib.rs's panel-layout state, seeded from the saved layout before the
sidebar exists, and collections_ui never touches it. Everything prefixed
library- (the grid's drag, selection and keyword state that the sidebar's
Rust also wires for cross-feature gestures like filing a selection into
a collection) stays on the window as well, since it belongs to the
library screen, not the sidebar.
This commit is contained in:
2026-09-20 21:07:25 +02:00
parent 14ed1dc410
commit f6ff5eabd9
9 changed files with 816 additions and 643 deletions
+122 -98
View File
@@ -16,7 +16,7 @@ use dr_types::{CollectionId, ImageId};
use slint::{ComponentHandle, Model as _};
use crate::library;
use crate::AppWindow;
use crate::{AppWindow, Collections};
use super::controller::{decide_drop, CollectionsController, Drop};
use super::drag::{arm_hold, arm_spring, collapse_spring_opened, compose_drag_image};
@@ -213,7 +213,9 @@ fn wire_selection(
let current = match library::read_member_order(cat, scope) {
Ok(current) => current,
Err(e) => {
w.set_collection_error(format!("reading the collection's order: {e}").into());
w.global::<Collections>().set_collection_error(
format!("reading the collection's order: {e}").into(),
);
return;
}
};
@@ -221,7 +223,8 @@ fn wire_selection(
match coll::set_order(cat.connection(), scope, &wanted) {
Ok(()) => {
w.set_collection_error(slint::SharedString::new());
w.global::<Collections>()
.set_collection_error(slint::SharedString::new());
w.set_library_status(
format!(
"{} photograph{} moved",
@@ -240,7 +243,8 @@ fn wire_selection(
}
Err(e) => {
drop(borrow);
w.set_collection_error(format!("reordering: {e}").into());
w.global::<Collections>()
.set_collection_error(format!("reordering: {e}").into());
}
}
});
@@ -339,7 +343,8 @@ fn wire_selection_filing(
let borrow = catalog.borrow();
let Some(cat) = borrow.as_ref() else {
w.set_collection_error("Open a library first.".into());
w.global::<Collections>()
.set_collection_error("Open a library first.".into());
return;
};
@@ -361,7 +366,8 @@ fn wire_selection_filing(
match made {
Ok(added) => {
w.set_collection_error(slint::SharedString::new());
w.global::<Collections>()
.set_collection_error(slint::SharedString::new());
w.set_library_status(
format!(
"{added} photograph{} in {name}",
@@ -386,7 +392,8 @@ fn wire_selection_filing(
}
Err(e) => {
drop(borrow);
w.set_collection_error(format!("making a collection: {e}").into());
w.global::<Collections>()
.set_collection_error(format!("making a collection: {e}").into());
}
}
});
@@ -449,7 +456,8 @@ fn wire_selection_filing(
match result {
Ok(filed) => {
w.set_collection_error(slint::SharedString::new());
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.
@@ -474,7 +482,9 @@ fn wire_selection_filing(
drop(borrow);
reload();
}
Err(e) => w.set_collection_error(format!("filing: {e}").into()),
Err(e) => w
.global::<Collections>()
.set_collection_error(format!("filing: {e}").into()),
}
});
}
@@ -528,11 +538,13 @@ fn wire_drag(
// that cares to read it; the drop itself uses the remembered press, since
// it is handed only the target.
{
window.on_collection_drag_payload(move |id| {
let mut data = slint::DataTransfer::default();
data.set_user_data(Rc::new(CollectionId(id as u64)));
data
});
window
.global::<Collections>()
.on_collection_drag_payload(move |id| {
let mut data = slint::DataTransfer::default();
data.set_user_data(Rc::new(CollectionId(id as u64)));
data
});
}
{
@@ -608,24 +620,26 @@ fn wire_drag(
let weak = window.as_weak();
let ctl = ctl.clone();
let catalog_for_spring = catalog.clone();
window.on_collection_drag_over(move |id, over| {
let Some(w) = weak.upgrade() else { return };
let id = CollectionId(id as u64);
window
.global::<Collections>()
.on_collection_drag_over(move |id, over| {
let Some(w) = weak.upgrade() else { return };
let id = CollectionId(id as u64);
if !over {
// Left this row. Cancel its pending expansion rather than
// letting it fire over whatever the pointer moved on to.
if *ctl.hover_id.borrow() == Some(id) {
*ctl.hover_id.borrow_mut() = None;
*ctl.spring_timer.borrow_mut() = None;
if !over {
// Left this row. Cancel its pending expansion rather than
// letting it fire over whatever the pointer moved on to.
if *ctl.hover_id.borrow() == Some(id) {
*ctl.hover_id.borrow_mut() = None;
*ctl.spring_timer.borrow_mut() = None;
}
return;
}
return;
}
*ctl.hover_id.borrow_mut() = Some(id);
let row = ctl.row_ids.borrow().iter().position(|&c| c == id);
arm_spring(&w, &ctl, &catalog_for_spring, row);
});
*ctl.hover_id.borrow_mut() = Some(id);
let row = ctl.row_ids.borrow().iter().position(|&c| c == id);
arm_spring(&w, &ctl, &catalog_for_spring, row);
});
}
// A drop. Slint hit-tested the release and `can-drop` already refused a
@@ -636,77 +650,85 @@ fn wire_drag(
let catalog = catalog.clone();
// No model refresh or reload here on purpose — see the note at the end
// of this handler. `drag-finished` owns those.
window.on_collection_dropped(move |id| {
let Some(w) = weak.upgrade() else { return };
let id = CollectionId(id as u64);
let carried = ctl.dragging.borrow().clone();
// Taken, not read: a remembered press must be spent by the drop it
// belongs to, or a later empty drop — a file dragged in from
// outside, say — would move a collection nobody touched.
let pressed = ctl.dragging_collection.borrow_mut().take();
window
.global::<Collections>()
.on_collection_dropped(move |id| {
let Some(w) = weak.upgrade() else { return };
let id = CollectionId(id as u64);
let carried = ctl.dragging.borrow().clone();
// Taken, not read: a remembered press must be spent by the drop it
// belongs to, or a later empty drop — a file dragged in from
// outside, say — would move a collection nobody touched.
let pressed = ctl.dragging_collection.borrow_mut().take();
let carried = match decide_drop(&carried, pressed, id) {
Drop::Nothing => return,
Drop::Reparent(source) => {
let result = {
let borrow = catalog.borrow();
let Some(cat) = borrow.as_ref() else { return };
coll::set_parent(cat.connection(), source, Some(id))
};
match result {
Ok(()) => {
w.set_collection_error(slint::SharedString::new());
w.set_library_status("Moved collection".into());
let carried = match decide_drop(&carried, pressed, id) {
Drop::Nothing => return,
Drop::Reparent(source) => {
let result = {
let borrow = catalog.borrow();
if let Some(cat) = borrow.as_ref() {
refresh_tree(&w, &ctl, cat);
let Some(cat) = borrow.as_ref() else { return };
coll::set_parent(cat.connection(), source, Some(id))
};
match result {
Ok(()) => {
w.global::<Collections>()
.set_collection_error(slint::SharedString::new());
w.set_library_status("Moved collection".into());
let borrow = catalog.borrow();
if let Some(cat) = borrow.as_ref() {
refresh_tree(&w, &ctl, cat);
}
}
// The catalog refuses a cycle rather than letting the
// tree walk spin. Saying so is better than a drop that
// silently does nothing.
Err(e) => w
.global::<Collections>()
.set_collection_error(format!("moving collection: {e}").into()),
}
// The catalog refuses a cycle rather than letting the
// tree walk spin. Saying so is better than a drop that
// silently does nothing.
Err(e) => w.set_collection_error(format!("moving collection: {e}").into()),
return;
}
return;
Drop::FileImages(images) => images,
};
// Scoped so the borrow is released before the spring cleanup below,
// which needs the catalog itself.
let result = {
let borrow = catalog.borrow();
let Some(cat) = borrow.as_ref() else { return };
coll::add_images(cat.connection(), id, &carried)
};
match result {
Ok(added) => {
w.global::<Collections>()
.set_collection_error(slint::SharedString::new());
// "Added 3 of 12" is the honest report when nine were
// already there; claiming 12 would teach the user to
// distrust the count.
let msg = if added == carried.len() {
format!("Added {added} to collection")
} else {
format!(
"Added {added} of {} — the rest were already there",
carried.len()
)
};
w.set_library_status(msg.into());
}
Err(e) => w
.global::<Collections>()
.set_collection_error(format!("adding to collection: {e}").into()),
}
Drop::FileImages(images) => images,
};
// Scoped so the borrow is released before the spring cleanup below,
// which needs the catalog itself.
let result = {
let borrow = catalog.borrow();
let Some(cat) = borrow.as_ref() else { return };
coll::add_images(cat.connection(), id, &carried)
};
match result {
Ok(added) => {
w.set_collection_error(slint::SharedString::new());
// "Added 3 of 12" is the honest report when nine were
// already there; claiming 12 would teach the user to
// distrust the count.
let msg = if added == carried.len() {
format!("Added {added} to collection")
} else {
format!(
"Added {added} of {} — the rest were already there",
carried.len()
)
};
w.set_library_status(msg.into());
}
Err(e) => w.set_collection_error(format!("adding to collection: {e}").into()),
}
// Recorded, not acted on. Every visible consequence — rebuilding
// the tree, refreshing the badges, rereading the grid — happens in
// `drag-finished`, because all three replace models that Slint is
// *currently walking* to deliver this very event. Tearing down a
// live `DropArea` from inside its own `dropped` handler is the same
// hazard `sync_rows` in lib.rs documents for the adjust panel.
*ctl.dropped_on.borrow_mut() = Some(id);
});
// Recorded, not acted on. Every visible consequence — rebuilding
// the tree, refreshing the badges, rereading the grid — happens in
// `drag-finished`, because all three replace models that Slint is
// *currently walking* to deliver this very event. Tearing down a
// live `DropArea` from inside its own `dropped` handler is the same
// hazard `sync_rows` in lib.rs documents for the adjust panel.
*ctl.dropped_on.borrow_mut() = Some(id);
});
}
// The drag ended: dropped, or abandoned. This is where the consequences of
@@ -784,7 +806,7 @@ fn wire_trash(
let weak = window.as_weak();
let ctl = ctl.clone();
let session = session.clone();
window.on_trash_dropped(move || {
window.global::<Collections>().on_trash_dropped(move || {
let Some(w) = weak.upgrade() else { return };
let carried = ctl.dragging.borrow().clone();
if carried.is_empty() {
@@ -795,7 +817,8 @@ fn wire_trash(
// still being walked to deliver this event.
*ctl.trash_requested.borrow_mut() = Some(carried);
let _ = session;
w.set_collection_error(slint::SharedString::new());
w.global::<Collections>()
.set_collection_error(slint::SharedString::new());
});
}
@@ -808,7 +831,7 @@ fn wire_trash(
let catalog = catalog.clone();
let session = session.clone();
let reload = on_scope_changed.clone();
window.on_trash_restore(move || {
window.global::<Collections>().on_trash_restore(move || {
let Some(w) = weak.upgrade() else { return };
let chosen = ctl.selected();
if chosen.is_empty() {
@@ -887,7 +910,7 @@ fn wire_trash_from_grid(
let catalog = catalog.clone();
let session = session.clone();
let reload = on_scope_changed.clone();
window.on_trash_empty(move || {
window.global::<Collections>().on_trash_empty(move || {
let Some(w) = weak.upgrade() else { return };
let conn = match session() {
@@ -904,7 +927,8 @@ fn wire_trash_from_grid(
let listed = match dr_catalog::trash::list(cat.connection(), usize::MAX) {
Ok(l) => l,
Err(e) => {
w.set_collection_error(format!("reading trash: {e}").into());
w.global::<Collections>()
.set_collection_error(format!("reading trash: {e}").into());
return;
}
};