Show what is selected, and let a collection be made of it
Build and test / Desktop (Linux) (push) Failing after 2m37s
Build and test / Layer separation (push) Successful in 26s
🐳 Android image / Build and push (push) Successful in 4s
Build and test / android-image (push) Successful in 5s
Traceability / Requirement traces (push) Successful in 40s
Build and test / Android (aarch64) (push) Failing after 6s

Three things a selection needed and did not have.

**Seeing it.** The count existed — "12 selected" — in the header row, which
scrolls sideways. On a tablet it sat past the right-hand edge along with every
button beside it, so a selection was something you could make and then not
see. A selection you cannot see is one you act on by accident.

**Putting it down.** The only way to clear one was "Done", which also leaves
select mode — so after filing forty photographs the next forty began by
re-entering a mode the user had not meant to leave. Clearing is now its own
action and keeps the mode.

**Filing it somewhere new.** Making a collection of a selection took four
steps: create one, find it in the tree, select the photographs again because
creating it changed the scope, then add them. It is one press, which is how a
selection is usually meant — it is gathered *because* it is going somewhere.

The new collection is created at the top level rather than inside the current
scope, unlike the tree's "+". A selection can be gathered from anywhere,
including across collections, so filing it under whichever one happens to be
open would put it somewhere its contents did not come from. It opens straight
into its name field, for the reason `collection_new` already does: the
placeholder name is nobody's choice, and making the user find the rename
afterwards is asking them to finish a job we started.

All of it on its own strip beside the date range's, appearing only while there
is a selection — the third control this session that was invisible for being
put in a row that scrolls.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-23 17:57:35 +02:00
co-authored by Claude Opus 5
parent ffc40c42d2
commit b0b6dd559a
4 changed files with 175 additions and 16 deletions
+5
View File
@@ -455,6 +455,9 @@ export component AppWindow inherits Window {
/// Whether the grid is narrowed to the timeline's visible span.
in-out property <bool> library-range-active: false;
callback library-toggle-date-range();
/// TRACES: FR-CAT-5
callback library-clear-selection();
callback library-collection-from-selection();
/// TRACES: FR-CAT-6
in property <string> library-range-from;
in property <string> library-range-to;
@@ -1321,6 +1324,8 @@ in property <bool> panel-visible: true;
toggle-local-only() => { root.library-toggle-local-only(); }
range-active: root.library-range-active;
toggle-date-range() => { root.library-toggle-date-range(); }
clear-selection() => { root.library-clear-selection(); }
collection-from-selection() => { root.library-collection-from-selection(); }
range-from: root.library-range-from;
range-to: root.library-range-to;
range-invalid: root.library-range-invalid;
+66
View File
@@ -1035,6 +1035,12 @@ export component LibraryGrid inherits Rectangle {
/// as the ctrl flag rather than as a third selection policy.
in property <bool> select-mode: false;
callback toggle-select-mode();
/// TRACES: FR-CAT-5
/// Drop the selection without leaving select mode.
callback clear-selection();
/// TRACES: FR-CAT-5
/// Make a new collection holding exactly what is selected.
callback collection-from-selection();
/// The drag payload: the selected image ids, wrapped by Rust. Called when a
/// drag starts, so it always reflects the selection as it is at that moment.
pure callback drag-payload() -> data-transfer;
@@ -1767,6 +1773,66 @@ export component LibraryGrid inherits Rectangle {
}
}
// --- what is selected, and what to do with it --------------------
//
// The count and these actions used to live only in the header row,
// which scrolls sideways: on a tablet "12 selected" and everything
// beside it sat past the right-hand edge, so a selection was something
// you could make and then not see. A selection you cannot see is one
// you act on by accident.
//
// Its own strip, like the date range's, appearing only while there is
// a selection — so it costs nothing the rest of the time, and when it
// is there it has the width of the window rather than the tail of a
// row that did not fit.
if root.selected-count > 0: Rectangle {
height: 40px;
background: Theme.surface;
HorizontalLayout {
width: 100%;
height: 100%;
padding-left: Theme.gap;
padding-right: Theme.gap;
spacing: Theme.gap-sm;
alignment: start;
// State rather than a label: it is what the buttons beside it
// act on.
Value {
text: root.selected-count + (root.selected-count == 1
? " photograph selected" : " photographs selected");
modified: true;
font-weight: 600;
vertical-alignment: center;
overflow: elide;
}
Rectangle { width: Theme.gap; }
// The way out that is not "undo every tap". Distinct from
// "Done", which leaves select mode entirely: clearing keeps the
// mode, so the next selection can start straight away.
Button {
text: "Clear";
y: (parent.height - self.height) / 2;
clicked => { root.clear-selection(); }
}
// Filing a selection into a collection that does not exist yet
// took four steps: make a collection, find it, select the
// photographs again, add them. One press instead, which is how
// a selection is usually meant.
Button {
text: "New collection from selection";
primary: true;
y: (parent.height - self.height) / 2;
clicked => { root.collection-from-selection(); }
}
}
}
// --- the date range's ends ---------------------------------------
//
// Its own strip, a sibling of the chips rather than a child of them.