Take the whole run a shift-click names, not the part that happens to be loaded
Shift-clicking two photographs selected only the cells between them that were in the loaded window. The grid is a window of a hundred or so over a library of twenty thousand, and `apply_press` resolved the range against that window — `ids[lo_row..=hi_row]`, clamped to what was there. Everything else in the run had no id anywhere in the UI, so it was silently dropped. The user cannot see that: the selection count is off screen along with the photographs, and the gesture only announces itself when the drop files a dozen images instead of two hundred. The two ends are *ordinals*, and only the catalog knows what lies between them. `read_ids_span` asks it, through the same predicates, the same rating filter and the same ordering the window itself is read with — an ordinal names a photograph only relative to an ordering, so a run taken through any other one is a run through a different library. That ordering is now a constant, `GRID_ORDER`, shared by the window, the trash's own order beside it, and the run: capture time first, with the file name breaking ties and nothing more. A card written by two cameras interleaves names that have nothing to do with each other, and what "everything between these two" means to a photographer is a stretch of an afternoon. The query is reached through a closure handed to `CollectionsController` at wiring time rather than a catalog handle, because the scope and the filter that bound the run belong to the grid's controller. `apply_press` stays a pure function of what it is given, which is what keeps the selection rules testable with no library open — and the tests pass a run that reads a plain slice. Where there is nothing to ask, the loaded window is still used: a poorer answer than the catalog's and a far better one than a gesture that appears to do nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+202
-67
@@ -86,6 +86,13 @@ impl PressUndo {
|
||||
}
|
||||
}
|
||||
|
||||
/// TRACES: FR-CAT-5
|
||||
/// How a run of grid ordinals is turned into the ids it names.
|
||||
///
|
||||
/// See [`CollectionsController::span_source`] for why this is supplied rather
|
||||
/// than reached for.
|
||||
type SpanIds = Rc<dyn Fn(usize, usize) -> Vec<ImageId>>;
|
||||
|
||||
/// Selection, drag, and tree state for the running window.
|
||||
///
|
||||
/// Everything is `RefCell` because Slint callbacks are `Fn`, not `FnMut`, and
|
||||
@@ -233,6 +240,18 @@ pub struct CollectionsController {
|
||||
/// the tree, the grid cells — and doing that from inside the `dropped`
|
||||
/// handler destroys the elements Slint is still using to deliver the event.
|
||||
dropped_on: RefCell<Option<CollectionId>>,
|
||||
/// TRACES: FR-CAT-5
|
||||
/// How a run of grid ordinals is turned into ids.
|
||||
///
|
||||
/// Supplied by [`crate::library_ui`] at wiring time, because the catalog,
|
||||
/// the scope and the rating filter — everything the run depends on — belong
|
||||
/// to the grid's controller, not to this one. Held as a closure rather than
|
||||
/// reached for through a handle so the selection rules stay testable with
|
||||
/// no library open: the tests pass a run that reads a plain slice.
|
||||
///
|
||||
/// `None` before wiring, which the caller reads as "nothing to ask" and
|
||||
/// falls back to the loaded window.
|
||||
span_source: RefCell<Option<SpanIds>>,
|
||||
/// Where the trash workers report what they are doing.
|
||||
///
|
||||
/// Shared with [`crate::library_ui`]: a delete and a scan are two jobs in
|
||||
@@ -318,6 +337,15 @@ impl CollectionsController {
|
||||
self.selection.borrow().iter().copied().collect()
|
||||
}
|
||||
|
||||
/// TRACES: FR-CAT-5
|
||||
/// The ids of grid rows `first..=last`, in the order the grid shows them.
|
||||
///
|
||||
/// Empty when nothing has been wired in — see [`Self::span_source`].
|
||||
fn span(&self, first: usize, last: usize) -> Vec<ImageId> {
|
||||
let source = self.span_source.borrow().clone();
|
||||
source.map(|f| f(first, last)).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Drop the selection — after a scrub, or when the scope changes.
|
||||
///
|
||||
/// Selection is by id and survives a window change, but a selection the
|
||||
@@ -359,6 +387,11 @@ impl CollectionsController {
|
||||
/// `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>,
|
||||
@@ -367,6 +400,7 @@ pub fn apply_press(
|
||||
row: usize,
|
||||
ctrl: bool,
|
||||
shift: bool,
|
||||
span: &dyn Fn(usize, usize) -> Vec<ImageId>,
|
||||
) {
|
||||
let Some(&id) = ids.get(row) else { return };
|
||||
let here = offset + row;
|
||||
@@ -401,22 +435,29 @@ pub fn apply_press(
|
||||
(here, from)
|
||||
};
|
||||
|
||||
// The range is described in ordinals and applied to the window: only
|
||||
// ids that are loaded can be selected, because selection is by id and
|
||||
// an image the catalog has not been asked for has no id here. A range
|
||||
// longer than the loaded window is therefore truncated to it rather
|
||||
// than silently selecting the wrong photographs — which is what
|
||||
// holding the anchor as a window row used to do.
|
||||
//
|
||||
// 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);
|
||||
for id in &ids[lo_row..=hi_row] {
|
||||
selection.insert(*id);
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -458,9 +499,10 @@ pub fn press_remembering_anchor(
|
||||
row: usize,
|
||||
ctrl: bool,
|
||||
shift: bool,
|
||||
span: &dyn Fn(usize, usize) -> Vec<ImageId>,
|
||||
) {
|
||||
let before = *anchor;
|
||||
apply_press(selection, anchor, ids, offset, row, ctrl, shift);
|
||||
apply_press(selection, anchor, ids, offset, row, ctrl, shift, span);
|
||||
// Only a press that *moved* the anchor updates this. The second tap of a
|
||||
// double tap lands on the cell the first tap made the anchor, so it changes
|
||||
// nothing and the origin survives to be extended from.
|
||||
@@ -476,6 +518,7 @@ pub fn press_remembering_anchor(
|
||||
/// selection mode, which the user entered deliberately, and a gesture that
|
||||
/// silently discarded the run they gathered a moment ago would make gathering
|
||||
/// two runs impossible.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn extend_to_row(
|
||||
selection: &mut BTreeSet<ImageId>,
|
||||
anchor: &mut Option<usize>,
|
||||
@@ -483,11 +526,12 @@ pub fn extend_to_row(
|
||||
ids: &[ImageId],
|
||||
offset: usize,
|
||||
row: usize,
|
||||
span: &dyn Fn(usize, usize) -> Vec<ImageId>,
|
||||
) {
|
||||
// Falling back to the current anchor makes a double tap with no history
|
||||
// select just that cell, which is what a double tap already did.
|
||||
*anchor = previous.or(*anchor);
|
||||
apply_press(selection, anchor, ids, offset, row, true, true);
|
||||
apply_press(selection, anchor, ids, offset, row, true, true, span);
|
||||
}
|
||||
|
||||
/// Apply a press and push the result into the grid — the whole of what a
|
||||
@@ -525,6 +569,7 @@ pub fn select_row(
|
||||
row,
|
||||
ctrl,
|
||||
shift,
|
||||
&|first, last| ctl.span(first, last),
|
||||
);
|
||||
ctl.previous_anchor.set(previous);
|
||||
// The cursor follows the press, so an arrow key after a click continues
|
||||
@@ -1401,16 +1446,19 @@ fn format_bytes(bytes: u64) -> String {
|
||||
/// `on_scope_changed` reloads the grid — that lives in [`crate::library_ui`],
|
||||
/// which owns the window and the thumbnail workers, so it is passed in rather
|
||||
/// than reached for.
|
||||
pub fn wire<S, R, C>(
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn wire<S, R, P, C>(
|
||||
window: &AppWindow,
|
||||
ctl: Rc<CollectionsController>,
|
||||
catalog: Rc<RefCell<Option<Catalog>>>,
|
||||
on_scope_changed: S,
|
||||
visible_ids: R,
|
||||
span_ids: P,
|
||||
session: C,
|
||||
) where
|
||||
S: Fn() + 'static,
|
||||
R: Fn() -> Vec<ImageId> + 'static,
|
||||
P: Fn(usize, usize) -> Vec<ImageId> + 'static,
|
||||
C: Fn() -> Option<(
|
||||
dr_sync_nextcloud::AppCredentials,
|
||||
dr_sync_nextcloud::Session,
|
||||
@@ -1421,6 +1469,9 @@ pub fn wire<S, R, C>(
|
||||
// make each of them a separate instantiation for no gain.
|
||||
let on_scope_changed: Rc<dyn Fn()> = Rc::new(on_scope_changed);
|
||||
let visible_ids = Rc::new(visible_ids);
|
||||
// A shift-click asks the catalog what lies between its two ends, and the
|
||||
// catalog belongs to the grid's controller — see `span_source`.
|
||||
*ctl.span_source.borrow_mut() = Some(Rc::new(span_ids));
|
||||
let session: Rc<
|
||||
dyn Fn() -> Option<(
|
||||
dr_sync_nextcloud::AppCredentials,
|
||||
@@ -1500,6 +1551,7 @@ pub fn wire<S, R, C>(
|
||||
&ids,
|
||||
offset,
|
||||
row as usize,
|
||||
&|first, last| ctl.span(first, last),
|
||||
);
|
||||
ctl.set_cursor(Some(offset + row as usize));
|
||||
sync_selection(&w, &ctl, &ids);
|
||||
@@ -2494,6 +2546,28 @@ mod tests {
|
||||
(1..=n).map(ImageId).collect()
|
||||
}
|
||||
|
||||
/// 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 dragging_a_collection_onto_another_moves_it() {
|
||||
// The gesture the tree rearrangement exists for: no images carried, a
|
||||
@@ -2535,11 +2609,12 @@ mod tests {
|
||||
#[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;
|
||||
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 0, false, false);
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 2, false, false);
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 0, false, false, &span);
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 2, false, false, &span);
|
||||
|
||||
assert_eq!(sel.iter().copied().collect::<Vec<_>>(), vec![ImageId(3)]);
|
||||
}
|
||||
@@ -2547,26 +2622,28 @@ mod tests {
|
||||
#[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;
|
||||
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 0, false, false);
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 3, true, false);
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 0, false, false, &span);
|
||||
apply_press(&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.
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 3, true, false);
|
||||
apply_press(&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;
|
||||
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 2, false, false);
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 6, false, true);
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 2, false, false, &span);
|
||||
apply_press(&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)));
|
||||
@@ -2575,11 +2652,12 @@ mod tests {
|
||||
#[test]
|
||||
fn shift_extends_backwards_too() {
|
||||
let all = ids(10);
|
||||
let span = library(&all);
|
||||
let mut sel = BTreeSet::new();
|
||||
let mut anchor = None;
|
||||
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 6, false, false);
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 2, false, true);
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 6, false, false, &span);
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 2, false, true, &span);
|
||||
assert_eq!(sel.len(), 5);
|
||||
}
|
||||
|
||||
@@ -2589,15 +2667,16 @@ mod tests {
|
||||
// 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;
|
||||
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 5, false, false);
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 15, false, true);
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 5, false, false, &span);
|
||||
apply_press(&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.
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 8, false, true);
|
||||
apply_press(&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");
|
||||
}
|
||||
@@ -2607,12 +2686,13 @@ mod tests {
|
||||
// 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;
|
||||
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 10, false, false);
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 14, false, true);
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 12, false, true);
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 10, false, false, &span);
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 14, false, true, &span);
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 12, false, true, &span);
|
||||
|
||||
assert_eq!(anchor, Some(10));
|
||||
assert_eq!(sel.len(), 3, "rows 10..=12");
|
||||
@@ -2627,7 +2707,17 @@ mod tests {
|
||||
all: &[ImageId],
|
||||
row: usize,
|
||||
) {
|
||||
press_remembering_anchor(sel, anchor, previous, all, 0, row, true, false);
|
||||
press_remembering_anchor(
|
||||
sel,
|
||||
anchor,
|
||||
previous,
|
||||
all,
|
||||
0,
|
||||
row,
|
||||
true,
|
||||
false,
|
||||
&library(all),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2638,6 +2728,7 @@ mod tests {
|
||||
// it on, the second turns it off — and the double tap that follows has
|
||||
// to select the range anyway.
|
||||
let all = ids(20);
|
||||
let span = library(&all);
|
||||
let mut sel = BTreeSet::new();
|
||||
let mut anchor = None;
|
||||
let mut previous = None;
|
||||
@@ -2653,6 +2744,7 @@ mod tests {
|
||||
4,
|
||||
false,
|
||||
false,
|
||||
&span,
|
||||
);
|
||||
|
||||
tap(&mut sel, &mut anchor, &mut previous, &all, 11);
|
||||
@@ -2663,7 +2755,7 @@ mod tests {
|
||||
job to select the range rather than to add one cell"
|
||||
);
|
||||
|
||||
extend_to_row(&mut sel, &mut anchor, previous, &all, 0, 11);
|
||||
extend_to_row(&mut sel, &mut anchor, previous, &all, 0, 11, &span);
|
||||
|
||||
assert_eq!(sel.len(), 8, "rows 4..=11");
|
||||
assert!(sel.contains(&ImageId(5)) && sel.contains(&ImageId(12)));
|
||||
@@ -2675,6 +2767,7 @@ mod tests {
|
||||
// user in selection mode is gathering, and the second gesture must not
|
||||
// throw away the first.
|
||||
let all = ids(30);
|
||||
let span = library(&all);
|
||||
let mut sel = BTreeSet::new();
|
||||
let mut anchor = None;
|
||||
let mut previous = None;
|
||||
@@ -2688,17 +2781,18 @@ mod tests {
|
||||
0,
|
||||
false,
|
||||
false,
|
||||
&span,
|
||||
);
|
||||
tap(&mut sel, &mut anchor, &mut previous, &all, 3);
|
||||
tap(&mut sel, &mut anchor, &mut previous, &all, 3);
|
||||
extend_to_row(&mut sel, &mut anchor, previous, &all, 0, 3);
|
||||
extend_to_row(&mut sel, &mut anchor, previous, &all, 0, 3, &span);
|
||||
assert_eq!(sel.len(), 4, "rows 0..=3");
|
||||
|
||||
// A second run, begun with a plain tap somewhere else.
|
||||
tap(&mut sel, &mut anchor, &mut previous, &all, 20);
|
||||
tap(&mut sel, &mut anchor, &mut previous, &all, 25);
|
||||
tap(&mut sel, &mut anchor, &mut previous, &all, 25);
|
||||
extend_to_row(&mut sel, &mut anchor, previous, &all, 0, 25);
|
||||
extend_to_row(&mut sel, &mut anchor, previous, &all, 0, 25, &span);
|
||||
|
||||
assert_eq!(sel.len(), 10, "rows 0..=3 and 20..=25");
|
||||
assert!(sel.contains(&ImageId(1)) && sel.contains(&ImageId(26)));
|
||||
@@ -2709,11 +2803,12 @@ mod tests {
|
||||
// The degenerate case: selection mode entered from the header's button
|
||||
// rather than by holding a cell, so nothing has anchored yet.
|
||||
let all = ids(10);
|
||||
let span = library(&all);
|
||||
let mut sel = BTreeSet::new();
|
||||
let mut anchor = None;
|
||||
let previous = None;
|
||||
|
||||
extend_to_row(&mut sel, &mut anchor, previous, &all, 0, 6);
|
||||
extend_to_row(&mut sel, &mut anchor, previous, &all, 0, 6, &span);
|
||||
assert_eq!(sel.iter().copied().collect::<Vec<_>>(), vec![ImageId(7)]);
|
||||
}
|
||||
|
||||
@@ -2722,16 +2817,17 @@ mod tests {
|
||||
// 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;
|
||||
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 0, false, false);
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 2, false, true);
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 0, false, false, &span);
|
||||
apply_press(&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.
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 10, true, false);
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 12, true, true);
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 10, true, false, &span);
|
||||
apply_press(&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)));
|
||||
@@ -2742,16 +2838,17 @@ mod tests {
|
||||
// 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;
|
||||
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 0, false, false);
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 1, true, false);
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 2, true, false);
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 0, false, false, &span);
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 1, true, false, &span);
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 2, true, false, &span);
|
||||
assert_eq!(sel.len(), 3);
|
||||
|
||||
// Pressing one of the three to begin a drag.
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 1, false, false);
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 1, false, false, &span);
|
||||
assert_eq!(sel.len(), 3, "the selection survived the press");
|
||||
}
|
||||
|
||||
@@ -2763,17 +2860,18 @@ mod tests {
|
||||
// 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.
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 1, false, false);
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 3, true, false);
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 1, false, false, &span);
|
||||
apply_press(&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(1), Some(3));
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 5, false, false);
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 5, false, false, &span);
|
||||
assert_ne!(
|
||||
(sel.clone(), anchor),
|
||||
before,
|
||||
@@ -2791,10 +2889,11 @@ mod tests {
|
||||
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;
|
||||
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 99, false, false);
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 99, false, false, &span);
|
||||
assert!(sel.is_empty());
|
||||
}
|
||||
|
||||
@@ -2806,6 +2905,7 @@ mod tests {
|
||||
// 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();
|
||||
|
||||
@@ -2813,12 +2913,12 @@ mod tests {
|
||||
let mut anchor = None;
|
||||
|
||||
// Anchor on the sixth image, in a window starting at the beginning.
|
||||
apply_press(&mut sel, &mut anchor, &first, 0, 5, false, false);
|
||||
apply_press(&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.
|
||||
apply_press(&mut sel, &mut anchor, &later, 4, 6, false, true);
|
||||
apply_press(&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");
|
||||
@@ -2827,51 +2927,86 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_range_reaching_outside_the_window_selects_what_is_loaded() {
|
||||
// Selection is by id, so a range can only cover images the window
|
||||
// holds. Truncating is the honest answer; the alternative — indexing
|
||||
// from the window's start as though it were the library's — selects
|
||||
// the wrong photographs and looks like it worked.
|
||||
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);
|
||||
|
||||
apply_press(&mut sel, &mut anchor, &window, 10, 3, false, true);
|
||||
apply_press(&mut sel, &mut anchor, &window, 10, 3, false, true, &span);
|
||||
|
||||
assert_eq!(sel.len(), 4, "ordinals 10..=13, the loaded part of 0..=13");
|
||||
assert!(sel.contains(&ImageId(11)) && sel.contains(&ImageId(14)));
|
||||
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_stops_at_the_window() {
|
||||
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. Both directions truncate to what is loaded,
|
||||
// and neither may wrap round to the other end of the window.
|
||||
// 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);
|
||||
|
||||
apply_press(&mut sel, &mut anchor, &window, 0, 2, false, true);
|
||||
apply_press(&mut sel, &mut anchor, &window, 0, 2, false, true, &span);
|
||||
|
||||
assert_eq!(sel.len(), 3, "ordinals 2..=4, the loaded part of 2..=25");
|
||||
assert!(sel.contains(&ImageId(3)) && sel.contains(&ImageId(5)));
|
||||
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);
|
||||
|
||||
apply_press(
|
||||
&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;
|
||||
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 3, false, true);
|
||||
apply_press(&mut sel, &mut anchor, &all, 0, 3, false, true, &span);
|
||||
assert_eq!(sel.iter().copied().collect::<Vec<_>>(), vec![ImageId(4)]);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user