diff --git a/ui/dr-ui/src/collections_ui.rs b/ui/dr-ui/src/collections_ui.rs index d59960c..1afd5e2 100644 --- a/ui/dr-ui/src/collections_ui.rs +++ b/ui/dr-ui/src/collections_ui.rs @@ -914,12 +914,19 @@ pub fn sync_lifted(window: &AppWindow, lifted: &[ImageId], ids: &[ImageId]) { pub fn sync_selection(window: &AppWindow, ctl: &Rc, ids: &[ImageId]) { let selection = ctl.selection.borrow(); let model = window.get_library_cells(); + // The anchor is an ordinal and the model is a window, so it marks a row + // only while the photograph it names is on screen. Off screen it marks + // nothing, which is the honest answer: the range still extends from there. + let offset = window.get_library_offset().max(0) as usize; + let anchor_row = ctl.anchor.borrow().and_then(|at| at.checked_sub(offset)); for (row, id) in ids.iter().enumerate() { let want = selection.contains(id); + let anchored = anchor_row == Some(row); if let Some(mut cell) = model.row_data(row) { - if cell.selected != want { + if cell.selected != want || cell.anchor != anchored { cell.selected = want; + cell.anchor = anchored; model.set_row_data(row, cell); } } diff --git a/ui/dr-ui/src/library_ui.rs b/ui/dr-ui/src/library_ui.rs index d1958fe..9b44509 100644 --- a/ui/dr-ui/src/library_ui.rs +++ b/ui/dr-ui/src/library_ui.rs @@ -2129,10 +2129,11 @@ fn load_window(window: &AppWindow, ctl: &Rc) { thumbnail: carried.map(|h| h.thumbnail.clone()).unwrap_or_default(), has_thumb: carried.is_some_and(|h| h.has_thumb), unavailable: carried.is_some_and(|h| h.unavailable), - // Both are filled straight after by `collections_ui`, which owns - // the selection and queries the badge counts for the whole window - // in one statement rather than one per cell. + // All three are filled straight after by `collections_ui`, + // which owns the selection and queries the badge counts for the + // whole window in one statement rather than one per cell. selected: false, + anchor: false, collection_count: 0, // Likewise filled by `sync_ratings` below — one query for the // window, not one per cell. diff --git a/ui/dr-ui/ui/library.slint b/ui/dr-ui/ui/library.slint index 5662a88..df3a0b0 100644 --- a/ui/dr-ui/ui/library.slint +++ b/ui/dr-ui/ui/library.slint @@ -430,6 +430,14 @@ export struct LibraryCell { // Part of the current selection. Selection is what a drag carries, so this // has to be per-cell state rather than a single "current" index. selected: bool, + // The cell a shift-click would extend *from* — the last one plainly + // clicked. Marked because otherwise the gesture has a hidden operand: the + // user picks the far end of a run with no way to see which end the range + // is being measured from, and a range that comes out wrong gives no clue + // why. At most one cell in the library carries this, and often none of + // them is on screen — which is itself the answer to "why did that select + // so much". + anchor: bool, // How many collections this image belongs to. An image can be in many at // once, and without a cue the grid gives no hint that a photograph has // already been filed — the user re-files it, or hunts for where it went. @@ -2687,6 +2695,22 @@ export component LibraryGrid inherits Rectangle { } } + // The end a shift-click measures from. An inner ring rather + // than a colour of its own: it has to be legible against a + // thumbnail of any brightness and must not read as a second + // kind of selection, which a hue would. + Rectangle { + visible: cell.anchor; + x: 4px; + y: 4px; + width: parent.width - 8px; + height: parent.height - 8px; + background: transparent; + border-width: 1px; + border-color: Theme.selected-ring; + border-radius: Theme.radius-sm; + } + // Selection only. The drag is the enclosing `DragArea`'s // business, and Slint keeps a click distinct from a drag for // us — which is exactly the arbitration the hand-rolled version