Merge: group the frames of one moment, and let a burst fold away

FR-CULL-5. Frames join a burst when they are adjacent in time and look
like the frame before them -- both, because time alone groups a whole
ceremony and similarity alone groups a studio setup across two days.
Adjacent pairs only, chained; there is no all-pairs step and there must
never be one.

No selection of any kind. The representative is the earliest frame, a
fact about the clock rather than a judgement about the photograph, and a
newly found burst arrives open, so the pass never takes a row off the
screen.

Verified: fmt, clippy --workspace --all-targets -D warnings, 346
dr-catalog tests, 511 dr-ui tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-29 22:07:05 +02:00
co-authored by Claude Opus 5
10 changed files with 2230 additions and 15 deletions
+84
View File
@@ -458,6 +458,16 @@ export struct LibraryCell {
// 0 unflagged, 1 pick, 2 reject. Independent of the stars: rejecting a
// four-star frame is a normal thing to do mid-cull.
flag: int,
// Frames in the burst this cell belongs to (FR-CULL-5), itself included; 0 where it
// belongs to none, which is most of a library. A burst that is collapsed
// draws only its representative, so on that cell this is the count of what
// is hidden behind it — the reason it is shown at all.
burst-count: int,
// Whether the group is currently open. Drawn differently rather than
// hidden: a burst the user has expanded is the one thing on screen that
// needs a way back, and a control that disappears once used is a control
// nobody finds twice.
burst-expanded: bool,
}
// The photo roll: the grid's loaded window along the foot of the develop view.
@@ -1155,6 +1165,11 @@ export component LibraryGrid inherits Rectangle {
callback cell-clicked(int);
/// A star was clicked on a cell: row, and the rating 0..5.
callback cell-rated(int, int);
/// The burst mark on a cell was clicked (FR-CULL-5): open the group, or fold it back
/// up. Which of the two is decided in Rust, from what the catalog says the
/// group is currently doing, so the mark cannot get out of step with the
/// query that actually hides the frames.
callback burst-toggled(int);
/// Whether the grid is currently listing the trash rather than the
/// library. Suppresses the per-cell trash target, which would be inert
/// there — `plan_trash` skips an already-trashed image — and offering a
@@ -2925,6 +2940,75 @@ export component LibraryGrid inherits Rectangle {
rate(n) => { root.cell-rated(i, n); }
trash() => { root.cell-trashed(i); }
}
// The burst mark (FR-CULL-5): how many frames this
// moment holds,
// and the way in and out of them.
//
// A child of `cell-touch` for exactly the reason the
// stars above are: a click here must not also reach
// `cell-clicked` and throw the user into develop, and
// children are hit-tested before the element they sit
// in. Unlike the stars it is never hidden — a collapsed
// burst is standing in for frames that are not on
// screen, and there has to be something visible saying
// so whether or not a pointer is anywhere near.
//
// Bottom left, clear of the centred star strip and of
// both top corners, which the flag and the collection
// badge already have.
if cell.burst-count > 1: Rectangle {
x: 6px;
y: parent.height - self.height - 26px;
width: 30px;
height: 18px;
// The pile behind the top card, drawn only while the
// group is folded up. It is the whole of the "there
// is more than one of these" cue; once the burst is
// open the frames themselves say it.
Rectangle {
x: 3px;
y: -3px;
width: parent.width - 3px;
height: parent.height;
visible: !cell.burst-expanded;
background: Theme.surface;
border-radius: Theme.radius-sm;
border-width: 1px;
border-color: Theme.rule;
}
Rectangle {
width: 100%;
height: 100%;
background: cell.burst-expanded ? Theme.selected
: Theme.surface;
border-radius: Theme.radius-sm;
border-width: 1px;
border-color: cell.burst-expanded ? Theme.selected-ring
: Theme.rule;
Text {
width: 100%;
height: 100%;
horizontal-alignment: center;
vertical-alignment: center;
// No "of": the number is the size of the
// group, and a cell this small cannot
// afford a word to say so.
text: cell.burst-count;
color: Theme.ink;
font-size: 10px;
font-weight: 700;
}
}
TouchArea {
mouse-cursor: pointer;
clicked => { root.burst-toggled(i); }
}
}
}
}
}