Let the photographer say which frame the burst stands for
`choose_representative` has been in the catalog since the grouping landed, with tests behind it and nothing calling it. So the frame a folded burst drew was always the earliest one, and the only way to disagree was to open the group and leave it open — which is to say there was no way to disagree at all, because a burst that stays open is a burst that was never collapsed. The earliest frame is the right default and it is deliberately not a judgement: nothing here scores a photograph, and FR-CULL-5 names the failure that rule avoids. But the whole point of a burst is that one of the twelve is better than the other eleven, and the person who knows which is the one looking at them. So a ring on each frame of an open group, ticked on the one the group folds to. It is drawn only while the burst is open, because that is the one moment the alternatives are on screen to be compared — offering the choice on a folded burst would be asking about frames it is hiding. Bottom right, opposite the count in the other corner, clear of the flag and the collection badge and, deliberately, of the trash target: a slip between the ring and the fifth star sets a rating, which is the harmless direction for an ambiguous press. The mark stays live on the frame that already wears it. A disabled TouchArea would let the press fall through to the cell behind it, so tapping the one ring that is ticked would have opened the photograph — and pressing it is a thing the user may mean anyway: it records the choice the default was making silently, which then survives a regroup that finds an earlier frame. Choosing repaints the badges instead of reloading the window, which is what separates it from folding a group up. Folding changes what the grid's query returns; this changes only which cell wears the tick, and the tick has to leave the frame that was carrying it, so the whole window is refilled in the one statement `sync_badges` already runs. The gesture is documented where FR-UI-4 requires it to be documented: in a tagged comment beside the control, which is the only copy. The gesture book, the gesture document and the requirements matrix are regenerated from the tree alongside it.
This commit is contained in:
+72
-4
@@ -366,15 +366,19 @@ pub fn sync_badges(window: &AppWindow, catalog: &Catalog, ids: &[ImageId]) {
|
||||
|
||||
let model = window.get_library_cells();
|
||||
for (row, id) in ids.iter().enumerate() {
|
||||
let (count, expanded) = match found.get(id) {
|
||||
Some(m) => (m.size as i32, m.expanded),
|
||||
let (count, expanded, representative) = match found.get(id) {
|
||||
Some(m) => (m.size as i32, m.expanded, m.representative),
|
||||
// Not in a burst at all, which is most of a library.
|
||||
None => (0, false),
|
||||
None => (0, false, false),
|
||||
};
|
||||
if let Some(mut cell) = model.row_data(row) {
|
||||
if cell.burst_count != count || cell.burst_expanded != expanded {
|
||||
if cell.burst_count != count
|
||||
|| cell.burst_expanded != expanded
|
||||
|| cell.burst_representative != representative
|
||||
{
|
||||
cell.burst_count = count;
|
||||
cell.burst_expanded = expanded;
|
||||
cell.burst_representative = representative;
|
||||
model.set_row_data(row, cell);
|
||||
}
|
||||
}
|
||||
@@ -411,6 +415,32 @@ pub fn toggle(catalog: &Catalog, image: ImageId) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Name the frame the burst a cell belongs to folds up to.
|
||||
///
|
||||
/// The whole of the arbitration is [`bursts::choose_representative`]'s: a group
|
||||
/// stands for one frame, so the previous pick has to go in the same transaction
|
||||
/// the new one arrives in, and the flag the grouping already carries has to be
|
||||
/// corrected there too or the cell would not change until the next pass.
|
||||
///
|
||||
/// Nothing here decides *which* frame deserves it, and nothing ever will — the
|
||||
/// default is the earliest one, which is a fact about the clock rather than a
|
||||
/// judgement about the photograph (FR-CULL-5). This is the photographer saying
|
||||
/// otherwise about a group they are looking at.
|
||||
///
|
||||
/// Returns whether the catalog took it, so a caller only repaints when there is
|
||||
/// something to repaint. Choosing a frame that is in no burst is not an error
|
||||
/// and writes nothing; it simply cannot arrive from the grid, where the mark is
|
||||
/// drawn on the members of an open group and nowhere else.
|
||||
pub fn choose(catalog: &Catalog, image: ImageId) -> bool {
|
||||
match bursts::choose_representative(catalog.connection(), image) {
|
||||
Ok(()) => true,
|
||||
Err(e) => {
|
||||
log::warn!("choosing the representative of image {}: {e}", image.0);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -528,4 +558,42 @@ mod tests {
|
||||
let (cat, _dir) = library("no-burst");
|
||||
assert!(!toggle(&cat, ImageId(1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn choosing_a_frame_moves_the_mark_the_grid_draws() {
|
||||
// The tick a cell wears is filled in from `memberships`, so that is
|
||||
// where the choice has to appear. A pick recorded only in `burst_pick`
|
||||
// would be honoured at the next pass and invisible until then, which is
|
||||
// the whole reason `choose_representative` corrects the grouping in the
|
||||
// same transaction.
|
||||
let (cat, dir) = library("choose");
|
||||
let conn = cat.connection();
|
||||
let store = ThumbStore::open(&dir).unwrap();
|
||||
let outstanding = bursts::images_without_signature(conn).unwrap();
|
||||
let (tx, _rx) = std::sync::mpsc::channel();
|
||||
assert_eq!(hash_all(conn, &store, &outstanding, &tx), 2);
|
||||
bursts::regroup(conn, Rules::default()).unwrap();
|
||||
|
||||
let mark = |id: ImageId| bursts::memberships(conn, &[id]).unwrap()[&id].representative;
|
||||
assert!(
|
||||
mark(ImageId(1)),
|
||||
"the earliest frame stands for it by default"
|
||||
);
|
||||
|
||||
assert!(choose(&cat, ImageId(2)));
|
||||
assert!(mark(ImageId(2)), "the chosen frame did not take the mark");
|
||||
assert!(
|
||||
!mark(ImageId(1)),
|
||||
"two frames cannot both stand for one moment"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn choosing_a_frame_that_is_in_no_burst_is_not_an_error() {
|
||||
// Unreachable from the grid, where the mark is drawn on the members of
|
||||
// an open group and nowhere else — but a lone frame arriving here is a
|
||||
// race with a regroup, not a fault.
|
||||
let (cat, _dir) = library("choose-no-burst");
|
||||
assert!(choose(&cat, ImageId(1)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +120,13 @@ pub const GESTURES: &[Gesture] = &[
|
||||
pointer: "Hover the cell, then click a star",
|
||||
keys: "0 to 5 on the selection",
|
||||
},
|
||||
Gesture {
|
||||
title: "Choose the frame a folded burst shows",
|
||||
section: "Library grid",
|
||||
touch: "Open the burst, then tap the ring on the frame you want",
|
||||
pointer: "Open the burst, then click the ring on the frame you want",
|
||||
keys: "",
|
||||
},
|
||||
Gesture {
|
||||
title: "Drop the selection but keep selecting",
|
||||
section: "Library grid",
|
||||
|
||||
@@ -2818,6 +2818,7 @@ fn load_window(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
||||
// photograph in a library is.
|
||||
burst_count: 0,
|
||||
burst_expanded: false,
|
||||
burst_representative: false,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
@@ -6136,6 +6137,41 @@ pub fn wire<F>(
|
||||
});
|
||||
}
|
||||
|
||||
// Name the frame an open burst folds to (FR-CULL-5). The default is the
|
||||
// earliest of them, chosen because it is a fact about the clock and not a
|
||||
// judgement about the photograph; this is the photographer, who is the only
|
||||
// one who knows which of the twelve is the keeper, saying otherwise.
|
||||
//
|
||||
// A repaint of the badges rather than a reload, which is what separates it
|
||||
// from folding a burst up: the grid's query returns the same rows either
|
||||
// way while the group is open, and only the mark on them moves. Refilled
|
||||
// for the whole window in one statement — the tick has to leave the frame
|
||||
// that was carrying it, and that frame is a different cell.
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
window.on_library_burst_representative_chosen(move |row| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let ids: Vec<dr_types::ImageId> = ctl
|
||||
.image_ids
|
||||
.borrow()
|
||||
.iter()
|
||||
.map(|id| dr_types::ImageId(*id as u64))
|
||||
.collect();
|
||||
let Some(id) = ids.get(row as usize).copied() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let borrow = ctl.catalog.borrow();
|
||||
let Some(catalog) = borrow.as_ref() else {
|
||||
return;
|
||||
};
|
||||
if crate::bursts::choose(catalog, id) {
|
||||
crate::bursts::sync_badges(&w, catalog, &ids);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// A rating or flag key. Applies to the whole selection, which is what
|
||||
// makes judging a run of frames one keystroke rather than forty.
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user