Files
dtourolle 402dcdc24c 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.
2026-09-20 21:42:48 +02:00

765 lines
29 KiB
Rust

//! A press, and the release that follows it: what selects, what waits for
//! the tap that confirms it, and how a gesture that turns out not to have
//! been a press is undone.
use std::collections::BTreeSet;
use std::rc::Rc;
use dr_types::ImageId;
use slint::ComponentHandle as _;
use crate::{AppWindow, Library};
use super::controller::CollectionsController;
use super::tree_sync::sync_selection;
/// The selection as it stood before a press, for [`cancel_press`].
pub(super) struct PressUndo {
selection: BTreeSet<ImageId>,
anchor: Option<usize>,
cursor: Option<usize>,
}
impl PressUndo {
/// Everything [`select_row`] is about to change.
///
/// The three are the whole of what a press touches, which is the property
/// the restore depends on and the reason it is worth a test of its own: a
/// press that grew a fourth piece of state would leave that piece behind
/// after a cancel, silently.
fn capture(
selection: &BTreeSet<ImageId>,
anchor: Option<usize>,
cursor: Option<usize>,
) -> Self {
Self {
selection: selection.clone(),
anchor,
cursor,
}
}
/// Put it all back. Returns the one the caller holds in a `Cell`.
fn restore(
self,
selection: &mut BTreeSet<ImageId>,
anchor: &mut Option<usize>,
) -> Option<usize> {
*selection = self.selection;
*anchor = self.anchor;
self.cursor
}
}
/// TRACES: FR-CAT-7 | FR-UI-4
/// What a press did, and what is left for the release to do.
///
/// Only one press has anything left over, and it is the one every multi-image
/// drag depends on — see [`Press::Deferred`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[must_use]
pub enum Press {
/// The selection is already what this press means. Nothing to finish.
Applied,
/// **Taking a photograph out of the selection waits for the release.**
///
/// Putting one *in* has to happen on the press: the drag that may follow
/// reads the selection to decide what it carries, and by the time the
/// finger lifts it is over the sidebar. Taking one out is the opposite —
/// nothing between the press and the release needs the image gone, and one
/// thing very much needs it to stay.
///
/// A plain press has said so since selection was written: pressing an
/// already-selected cell leaves the selection alone. Ctrl did not, and on a
/// tablet *every* press is a ctrl-press — that is what selection mode is.
/// So grabbing one of forty selected photographs deselected it on the way
/// down; the drag that followed found the cell under the finger no longer
/// in the selection, took that to mean an unselected image was being
/// dragged, and carried it alone. Forty photographs became one, and the
/// only clue was the grabbed cell's ring blinking out.
///
/// So the removal is handed back for the *click* to apply, and a click
/// fires only for a press that stayed put — never for one that became a
/// drag. See `tap-slop` in `library.slint`.
Deferred(ImageId),
}
/// Apply a press to the selection.
///
/// Split from the callback so the policy is testable without a window: this is
/// the part a user notices being wrong.
///
/// - **plain** — replace the selection with this one image
/// - **ctrl** — add this image to the selection, keeping the rest, and move the
/// anchor here; taking one *out* is [deferred](Press::Deferred) to the release
/// - **shift** — select the range from the anchor to here, *replacing* what was
/// selected; the anchor stays put, so an overshoot is corrected by
/// shift-clicking the right cell rather than starting again
/// - **ctrl+shift** — the same range, *added* to the selection, for picking up a
/// second run without losing the first
///
/// A press on an image that is *already* selected never removes it here —
/// plainly or with ctrl. That is what makes dragging a multi-selection possible
/// at all: the press that begins the drag would otherwise take the grabbed
/// photograph out from under it.
///
/// `ids` is the **loaded window**, `offset` where it starts in the library and
/// `row` a position within it; the anchor is kept as `offset + row`, an
/// ordinal that still names the same photograph after the window has moved.
///
/// `span` turns a run of ordinals into the ids it names — the catalog's job,
/// since the run is mostly not loaded. Passed in rather than reached for so
/// this stays a pure function of what it is given.
#[allow(clippy::too_many_arguments)]
pub fn apply_press(
selection: &mut BTreeSet<ImageId>,
anchor: &mut Option<usize>,
ids: &[ImageId],
offset: usize,
row: usize,
ctrl: bool,
shift: bool,
span: &dyn Fn(usize, usize) -> Vec<ImageId>,
) -> Press {
let Some(&id) = ids.get(row) else {
return Press::Applied;
};
let here = offset + row;
if shift {
let Some(from) = *anchor else {
// No anchor to extend from: behave like a plain click and become
// the anchor, so the *next* shift-click has a range to describe.
selection.clear();
selection.insert(id);
*anchor = Some(here);
return Press::Applied;
};
// The anchor deliberately does **not** move. Shift-clicking again
// re-describes the range from the same origin, so a user who overshoots
// corrects by shift-clicking the right cell rather than starting over.
// That also means the previous range must be cleared first — extending
// without clearing turns a correction into a union, and the user ends up
// dragging cells they thought they had deselected.
//
// Ctrl+shift is the exception: it *adds* a range to what is already
// selected, which is how a second run is picked up without losing the
// first.
if !ctrl {
selection.clear();
}
let (lo, hi) = if from <= here {
(from, here)
} else {
(here, from)
};
// The run is described in ordinals and resolved by the catalog, which
// is the only thing that knows what lies between them. Selection is by
// id, and an image outside the loaded window has no id here — so an
// earlier version truncated the run to what was on screen, and
// shift-clicking two ends of a morning selected the dozen cells that
// happened to be loaded. The user cannot see that they did not get
// what they asked for until the drop files a dozen photographs
// instead of two hundred.
let mut run = span(lo, hi);
if run.is_empty() {
// Nothing to ask — no library open, or a query that failed. The
// loaded window is a poorer answer than the catalog's and a far
// better one than selecting nothing.
//
// The slice is always in range and needs no guard: `ids.get(row)`
// succeeded, so the window is non-empty and `here` is inside it,
// and `here` is one of the two bounds.
let last = ids.len() - 1;
let lo_row = lo.saturating_sub(offset);
let hi_row = hi.saturating_sub(offset).min(last);
run = ids[lo_row..=hi_row].to_vec();
}
selection.extend(run);
return Press::Applied;
}
if ctrl {
*anchor = Some(here);
// Taking one out waits for the release — see [`Press::Deferred`].
if selection.contains(&id) {
return Press::Deferred(id);
}
selection.insert(id);
return Press::Applied;
}
// Plain press on something already selected: leave it. The drag that may
// follow carries the whole selection, and collapsing it here would make a
// multi-image drag impossible to start.
if selection.contains(&id) {
*anchor = Some(here);
return Press::Applied;
}
selection.clear();
selection.insert(id);
*anchor = Some(here);
Press::Applied
}
/// Apply a press and push the result into the grid — the whole of what a
/// click, or an arrow key, does to the selection.
///
/// Shared so the keyboard and the pointer cannot drift: they are the same
/// gesture reached two ways, and the moment one of them grew its own copy of
/// the selection policy, "click here, shift+down twice" would stop meaning
/// what "click here, shift-click there" means.
pub fn select_row(
window: &AppWindow,
ctl: &Rc<CollectionsController>,
ids: &[ImageId],
offset: usize,
row: usize,
ctrl: bool,
shift: bool,
) {
// Before anything moves — see `press_undo`. A press that turns out to be
// the opening finger of a pinch is undone from here.
*ctl.press_undo.borrow_mut() = Some(PressUndo::capture(
&ctl.selection.borrow(),
*ctl.anchor.borrow(),
ctl.cursor(),
));
// Whatever the last press left pending is answered by this one: two
// presses without a click in between means the first never was a tap.
ctl.pending_toggle.set(None);
if let Press::Deferred(id) = apply_press(
&mut ctl.selection.borrow_mut(),
&mut ctl.anchor.borrow_mut(),
ids,
offset,
row,
ctrl,
shift,
&|first, last| ctl.span(first, last),
) {
ctl.pending_toggle.set(Some(id));
}
// The cursor follows the press, so an arrow key after a click continues
// from the cell that was clicked rather than from wherever the keyboard
// was last.
ctl.set_cursor(Some(offset + row));
sync_selection(window, ctl, ids);
}
/// TRACES: FR-CAT-7 | FR-UI-4
/// Finish a press that turned out to be a tap.
///
/// The other half of [`Press::Deferred`]: a ctrl-press on an already-selected
/// photograph leaves it in, because the drag that may follow has to be able to
/// carry it, and this takes it out again once the release has proved there was
/// no drag.
///
/// Called from the click, which Slint reports only for a press that stayed
/// within `tap-slop` of where it landed — so a press that became a drag never
/// reaches here, and a press taken over by the Flickable never reaches here
/// either. Both leave the pending removal to be dropped by the next press.
pub fn commit_press(window: &AppWindow, ctl: &Rc<CollectionsController>, ids: &[ImageId]) {
let Some(id) = ctl.pending_toggle.take() else {
return;
};
ctl.selection.borrow_mut().remove(&id);
sync_selection(window, ctl, ids);
}
/// TRACES: FR-UI-4
/// Put the selection back as it was before the most recent press.
///
/// For the gesture that begins as a press and turns out to be something else.
/// A pinch is the case that matters: it starts as one finger on a cell, which
/// selects it, and only becomes a pinch when the second finger lands — by
/// which point the user has selected a photograph they were not reaching for.
///
/// Idempotent, and a no-op when there is nothing to undo, so it is safe to
/// call on every gesture start rather than only on the ones that need it.
pub fn cancel_press(window: &AppWindow, ctl: &Rc<CollectionsController>, ids: &[ImageId]) {
// The press is being unmade, so what it left for the release to finish is
// unmade with it. Cleared before the early return: a press that changed
// nothing to undo can still have deferred a removal — and a finger that
// held long enough to pick a photograph up before the second one landed
// 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.global::<Library>().set_library_held_row(-1);
let Some(undo) = ctl.press_undo.borrow_mut().take() else {
return;
};
let cursor = undo.restore(
&mut ctl.selection.borrow_mut(),
&mut ctl.anchor.borrow_mut(),
);
ctl.set_cursor(cursor);
sync_selection(window, ctl, ids);
}
#[cfg(test)]
mod tests {
use crate::collections_ui::test_support::*;
use super::*;
/// A press and the release that follows it — which is what a click is.
///
/// These tests are about what a *user* sees, and a user only ever presses
/// and lets go. Calling [`apply_press`] alone would drop the half of the
/// policy that waits for the release ([`Press::Deferred`]) and quietly
/// assert the wrong thing about ctrl.
#[allow(clippy::too_many_arguments)]
fn click(
selection: &mut BTreeSet<ImageId>,
anchor: &mut Option<usize>,
ids: &[ImageId],
offset: usize,
row: usize,
ctrl: bool,
shift: bool,
span: &dyn Fn(usize, usize) -> Vec<ImageId>,
) {
if let Press::Deferred(id) =
apply_press(selection, anchor, ids, offset, row, ctrl, shift, span)
{
selection.remove(&id);
}
}
/// The catalog's answer to "what is between these two ordinals".
///
/// Stands in for [`crate::library_ui::LibraryController::ids_in_span`],
/// which does the same thing with a `LIMIT`/`OFFSET`: the whole library is
/// available to the query whatever the grid happens to have loaded, and an
/// ordinal indexes it directly.
fn library(all: &[ImageId]) -> impl Fn(usize, usize) -> Vec<ImageId> + '_ {
move |first, last| {
all.iter()
.copied()
.skip(first)
.take((last + 1).saturating_sub(first))
.collect()
}
}
/// No catalog to ask — what a press sees before a library is open, and if
/// the query fails.
fn unloaded(_first: usize, _last: usize) -> Vec<ImageId> {
Vec::new()
}
#[test]
fn a_plain_press_replaces_the_selection() {
let all = ids(5);
let span = library(&all);
let mut sel = BTreeSet::new();
let mut anchor = None;
click(&mut sel, &mut anchor, &all, 0, 0, false, false, &span);
click(&mut sel, &mut anchor, &all, 0, 2, false, false, &span);
assert_eq!(sel.iter().copied().collect::<Vec<_>>(), vec![ImageId(3)]);
}
#[test]
fn ctrl_press_adds_and_then_removes() {
let all = ids(5);
let span = library(&all);
let mut sel = BTreeSet::new();
let mut anchor = None;
click(&mut sel, &mut anchor, &all, 0, 0, false, false, &span);
click(&mut sel, &mut anchor, &all, 0, 3, true, false, &span);
assert_eq!(sel.len(), 2);
// Toggling: a second ctrl-press on the same cell takes it out again.
click(&mut sel, &mut anchor, &all, 0, 3, true, false, &span);
assert_eq!(sel.iter().copied().collect::<Vec<_>>(), vec![ImageId(1)]);
}
#[test]
fn shift_press_extends_a_contiguous_range() {
let all = ids(10);
let span = library(&all);
let mut sel = BTreeSet::new();
let mut anchor = None;
click(&mut sel, &mut anchor, &all, 0, 2, false, false, &span);
click(&mut sel, &mut anchor, &all, 0, 6, false, true, &span);
assert_eq!(sel.len(), 5, "rows 2..=6 inclusive");
assert!(sel.contains(&ImageId(3)) && sel.contains(&ImageId(7)));
}
#[test]
fn shift_extends_backwards_too() {
let all = ids(10);
let span = library(&all);
let mut sel = BTreeSet::new();
let mut anchor = None;
click(&mut sel, &mut anchor, &all, 0, 6, false, false, &span);
click(&mut sel, &mut anchor, &all, 0, 2, false, true, &span);
assert_eq!(sel.len(), 5);
}
#[test]
fn a_second_shift_click_re_describes_the_range_rather_than_adding_to_it() {
// Overshooting and correcting is the common case. Extending without
// clearing would turn the correction into a union, and the user would
// drag cells they believed they had just deselected.
let all = ids(20);
let span = library(&all);
let mut sel = BTreeSet::new();
let mut anchor = None;
click(&mut sel, &mut anchor, &all, 0, 5, false, false, &span);
click(&mut sel, &mut anchor, &all, 0, 15, false, true, &span);
assert_eq!(sel.len(), 11, "rows 5..=15");
// Corrected to a shorter range from the same anchor.
click(&mut sel, &mut anchor, &all, 0, 8, false, true, &span);
assert_eq!(sel.len(), 4, "rows 5..=8, and nothing from the first range");
assert!(!sel.contains(&ImageId(16)), "row 15 is no longer selected");
}
#[test]
fn the_anchor_stays_put_across_shift_clicks() {
// If the anchor moved to each shift-click, a range could only ever be
// grown, never corrected inward.
let all = ids(20);
let span = library(&all);
let mut sel = BTreeSet::new();
let mut anchor = None;
click(&mut sel, &mut anchor, &all, 0, 10, false, false, &span);
click(&mut sel, &mut anchor, &all, 0, 14, false, true, &span);
click(&mut sel, &mut anchor, &all, 0, 12, false, true, &span);
assert_eq!(anchor, Some(10));
assert_eq!(sel.len(), 3, "rows 10..=12");
}
/// TRACES: FR-UI-2 | FR-UI-4
/// Two taps on one cell put it back where it started, and take nothing else.
///
/// This is the behaviour that replaced the double-tap range. That gesture
/// selected everything between the cell and wherever the selection began —
/// silently, with no visible state, from a thing a hand does by accident.
/// "Select to…" does the same job and says so first, so a double tap is now
/// two toggles and nothing more: the only outcome a user can predict from
/// what is on the screen.
#[test]
fn two_taps_on_one_cell_cancel_out_and_take_no_range() {
let all = ids(30);
let span = library(&all);
let mut sel = BTreeSet::new();
let mut anchor = None;
// Selecting began somewhere else, so a range gesture would have had an
// anchor to sweep from.
click(&mut sel, &mut anchor, &all, 0, 4, false, false, &span);
assert_eq!(sel.len(), 1);
tap(&mut sel, &mut anchor, &all, 11);
tap(&mut sel, &mut anchor, &all, 11);
assert_eq!(
sel.iter().copied().collect::<Vec<_>>(),
vec![ImageId(5)],
"a double tap took a run instead of toggling one cell twice"
);
}
/// One tap in selection mode: a press reported as ctrl-held, which is what
/// `library.slint` sends while the mode is on.
fn tap(sel: &mut BTreeSet<ImageId>, anchor: &mut Option<usize>, all: &[ImageId], row: usize) {
click(sel, anchor, all, 0, row, true, false, &library(all));
}
/// TRACES: FR-CAT-7 | FR-UI-4
/// The bug that made a forty-image drag file one photograph.
///
/// In selection mode every press arrives as a ctrl-press, so grabbing one
/// of the selected cells to drag them all used to *deselect* the cell being
/// grabbed. `drag-started` then saw a press on an image that was not in the
/// selection, concluded that was the gesture, and carried it alone.
#[test]
fn grabbing_a_selected_cell_leaves_the_whole_selection_to_drag() {
let all = ids(20);
let span = library(&all);
let mut sel = BTreeSet::new();
let mut anchor = None;
// Three photographs picked in selection mode, which reports ctrl.
for row in [2, 5, 9] {
click(&mut sel, &mut anchor, &all, 0, row, true, false, &span);
}
assert_eq!(sel.len(), 3);
// The press that begins the drag, on one of the three.
let outcome = apply_press(&mut sel, &mut anchor, &all, 0, 5, true, false, &span);
assert_eq!(
outcome,
Press::Deferred(ImageId(6)),
"the removal has to be handed back, not performed"
);
assert_eq!(
sel.len(),
3,
"the drag reads the selection next, and all three have to still be in it"
);
assert!(sel.contains(&ImageId(6)), "least of all the one being held");
}
/// And the other half: with no drag, the release still takes it out.
///
/// A tap in selection mode has to toggle, or there is no way to correct a
/// mis-tap short of leaving the mode.
#[test]
fn a_tap_on_a_selected_cell_still_takes_it_out() {
let all = ids(20);
let span = library(&all);
let mut sel = BTreeSet::new();
let mut anchor = None;
for row in [2, 5, 9] {
click(&mut sel, &mut anchor, &all, 0, row, true, false, &span);
}
click(&mut sel, &mut anchor, &all, 0, 5, true, false, &span);
assert_eq!(
sel.iter().copied().collect::<Vec<_>>(),
vec![ImageId(3), ImageId(10)],
"the tapped photograph is out and the other two stayed"
);
}
/// The anchor moves on the press whether or not the removal does.
///
/// "Select to…" measures from the anchor, and a run taken after tapping a
/// selected cell has to start where the user last touched — otherwise the
/// gesture sweeps from wherever the anchor happened to be left.
#[test]
fn a_deferred_press_still_moves_the_anchor() {
let all = ids(20);
let span = library(&all);
let mut sel = BTreeSet::new();
let mut anchor = None;
click(&mut sel, &mut anchor, &all, 0, 3, true, false, &span);
let _ = apply_press(&mut sel, &mut anchor, &all, 0, 3, true, false, &span);
assert_eq!(anchor, Some(3));
}
#[test]
fn ctrl_shift_adds_a_second_range_to_the_selection() {
// Picking up a second run without losing the first: the one case where
// a shift-click must not clear.
let all = ids(20);
let span = library(&all);
let mut sel = BTreeSet::new();
let mut anchor = None;
click(&mut sel, &mut anchor, &all, 0, 0, false, false, &span);
click(&mut sel, &mut anchor, &all, 0, 2, false, true, &span);
assert_eq!(sel.len(), 3);
// A new anchor by ctrl-click, then a ctrl+shift range from it.
click(&mut sel, &mut anchor, &all, 0, 10, true, false, &span);
click(&mut sel, &mut anchor, &all, 0, 12, true, true, &span);
assert_eq!(sel.len(), 6, "rows 0..=2 and 10..=12");
assert!(sel.contains(&ImageId(1)) && sel.contains(&ImageId(13)));
}
#[test]
fn a_plain_press_on_a_selected_cell_keeps_the_selection() {
// This is what makes a multi-image drag possible: the press that starts
// the drag must not collapse what it is about to carry.
let all = ids(5);
let span = library(&all);
let mut sel = BTreeSet::new();
let mut anchor = None;
click(&mut sel, &mut anchor, &all, 0, 0, false, false, &span);
click(&mut sel, &mut anchor, &all, 0, 1, true, false, &span);
click(&mut sel, &mut anchor, &all, 0, 2, true, false, &span);
assert_eq!(sel.len(), 3);
// Pressing one of the three to begin a drag.
click(&mut sel, &mut anchor, &all, 0, 1, false, false, &span);
assert_eq!(sel.len(), 3, "the selection survived the press");
}
#[test]
fn a_press_undone_leaves_the_selection_exactly_as_it_was() {
// A pinch opens as one finger on a cell, so by the time it is known to
// be a pinch a photograph has been selected that the user was only
// reaching past. Undoing it has to be *exact*: a cancel that restored
// the selection but left the anchor moved would make the next
// shift-click select a run from a cell nobody pointed at.
let all = ids(6);
let span = library(&all);
let mut sel = BTreeSet::new();
let mut anchor = None;
// A selection built the ordinary way, and the state it leaves behind.
click(&mut sel, &mut anchor, &all, 0, 1, false, false, &span);
click(&mut sel, &mut anchor, &all, 0, 3, true, false, &span);
let before = (sel.clone(), anchor);
// The finger that opens a pinch.
let undo = PressUndo::capture(&sel, anchor, Some(3));
click(&mut sel, &mut anchor, &all, 0, 5, false, false, &span);
assert_ne!(
(sel.clone(), anchor),
before,
"the press has to change something, or this proves nothing"
);
let cursor = undo.restore(&mut sel, &mut anchor);
assert_eq!((sel, anchor), before, "selection and anchor are back");
assert_eq!(cursor, Some(3), "and the keyboard cursor");
}
#[test]
fn a_press_past_the_end_of_the_window_is_ignored() {
// The grid is windowed and a stale row index can arrive after a scrub.
let all = ids(3);
let span = library(&all);
let mut sel = BTreeSet::new();
let mut anchor = None;
click(&mut sel, &mut anchor, &all, 0, 99, false, false, &span);
assert!(sel.is_empty());
}
#[test]
fn an_anchor_survives_the_window_moving_under_it() {
// The grid is a window over the catalog, and the window moves whenever
// the user scrolls. Held as a row, an anchor set before a scroll
// described a range from whatever photograph had since drifted into
// that row — so shift-clicking after scrolling selected a run the user
// never pointed at, silently and with no way to tell.
let all = ids(20);
let span = library(&all);
let first: Vec<_> = all[0..10].to_vec();
let later: Vec<_> = all[4..14].to_vec();
let mut sel = BTreeSet::new();
let mut anchor = None;
// Anchor on the sixth image, in a window starting at the beginning.
click(&mut sel, &mut anchor, &first, 0, 5, false, false, &span);
assert_eq!(anchor, Some(5), "the anchor is an ordinal, not a row");
// The user scrolls — the window now starts four images in — and
// shift-clicks the image at ordinal 10.
click(&mut sel, &mut anchor, &later, 4, 6, false, true, &span);
assert_eq!(sel.len(), 6, "ordinals 5..=10");
assert!(sel.contains(&ImageId(6)), "the anchored image is still in");
assert!(sel.contains(&ImageId(11)), "up to the one shift-clicked");
assert!(!sel.contains(&ImageId(5)), "and nothing before the anchor");
}
#[test]
fn a_range_reaching_outside_the_window_selects_the_whole_run() {
// The bug this exists for: the anchor is far above the loaded window,
// so all but four of the photographs in the range are off screen. They
// are still what the user asked for, and the catalog is what knows
// their ids — reading the range from the window selected the four that
// happened to be loaded and looked as though it had worked.
let all = ids(30);
let span = library(&all);
let window: Vec<_> = all[10..20].to_vec();
let mut sel = BTreeSet::new();
let mut anchor = Some(0);
click(&mut sel, &mut anchor, &window, 10, 3, false, true, &span);
assert_eq!(sel.len(), 14, "ordinals 0..=13, loaded or not");
assert!(
sel.contains(&ImageId(1)),
"the anchored image, never loaded"
);
assert!(sel.contains(&ImageId(14)), "up to the cell pressed");
assert!(!sel.contains(&ImageId(15)), "and no further");
}
#[test]
fn a_range_running_off_the_far_end_is_whole_too() {
// The mirror of the case above, with the anchor *ahead* of the press
// instead of behind it. Neither direction may stop at the window, and
// neither may wrap round to the other end of it.
let all = ids(30);
let span = library(&all);
let window: Vec<_> = all[0..5].to_vec();
let mut sel = BTreeSet::new();
let mut anchor = Some(25);
click(&mut sel, &mut anchor, &window, 0, 2, false, true, &span);
assert_eq!(sel.len(), 24, "ordinals 2..=25");
assert!(sel.contains(&ImageId(3)) && sel.contains(&ImageId(26)));
assert!(
!sel.contains(&ImageId(1)),
"nothing before the pressed cell"
);
}
#[test]
fn a_range_with_no_catalog_to_ask_falls_back_to_the_window() {
// Before a library is open, and if the query fails. What is loaded is
// a poorer answer than the catalog's and a far better one than a
// gesture that appears to do nothing.
let all = ids(30);
let window: Vec<_> = all[10..20].to_vec();
let mut sel = BTreeSet::new();
let mut anchor = Some(0);
click(
&mut sel,
&mut anchor,
&window,
10,
3,
false,
true,
&unloaded,
);
assert_eq!(sel.len(), 4, "ordinals 10..=13, the loaded part of 0..=13");
assert!(sel.contains(&ImageId(11)) && sel.contains(&ImageId(14)));
}
#[test]
fn shift_without_an_anchor_selects_just_the_one() {
let all = ids(5);
let span = library(&all);
let mut sel = BTreeSet::new();
let mut anchor = None;
click(&mut sel, &mut anchor, &all, 0, 3, false, true, &span);
assert_eq!(sel.iter().copied().collect::<Vec<_>>(), vec![ImageId(4)]);
}
}