Give a panorama a wide cell in the grid

A 4:1 composite drawn in one square cell is a strip a few pixels high.
A photograph about twice as wide as it is tall (1.9 and up) now spans
two columns, three from 2.9, with a thumbnail class of its own whose
long edge is sized for that width; on the tablet, or where the columns
are too few to put it beside anything, it takes the whole row.

Rows are computed in one place, library_ui::layout. The grid is a
lattice of slots: each cell is drawn at the slot Rust gives it, and a
wide cell that would not fit in what is left of a row starts the next
one, leaving the gap empty so the grid still reads in capture order.
The scrollbar spans the slots, a scroll reports a slot that the layout
turns back into an ordinal, and scrubs, restores and the cursor go
through the same conversion. Up and down step by rows through the
layout rather than by a row's worth of ordinals; left and right, a
shift-click's run, burst folding and the timeline are ordinal-based
and unchanged.

The window's own read carries each photograph's w and h, so the cells
know their shape with no query per cell. Where the wide ones sit in
the whole list is one query, run when what the grid lists changes or
when a window finds the layout out of date, and a library with no
panorama answers it from a partial index created on first use
(images_wide), not a schema bump. The merge makes the wide thumbnail
for a wide composite along with the others.
This commit is contained in:
2026-09-28 19:57:37 -04:00
parent ce5b7d72e3
commit ae4e1a0f07
13 changed files with 878 additions and 89 deletions
+5
View File
@@ -1463,6 +1463,7 @@ in property <bool> panel-visible: true;
expanded: root.expanded;
cells: Library.library-cells;
total: Library.library-total;
total-slots: Library.library-total-slots;
scanning: Library.library-scanning;
opening: Library.library-opening;
scan-status: Library.library-status;
@@ -1584,9 +1585,13 @@ in property <bool> panel-visible: true;
assign-keyword(word) => { Library.library-assign-keyword(word); }
unassign-keyword(word) => { Library.library-unassign-keyword(word); }
cursor: Library.library-cursor;
cursor-slot: Library.library-cursor-slot;
move-cursor(delta, extend) => {
Library.library-move-cursor(delta, extend);
}
move-cursor-rows(rows, extend) => {
Library.library-move-cursor-rows(rows, extend);
}
open-cursor() => { Library.library-open-cursor(); }
drag-image: Library.library-drag-image;
drag-payload() => { return Library.library-drag-payload(); }
+52 -17
View File
@@ -483,6 +483,13 @@ export struct LibraryCell {
// the one cell on screen is the representative by construction, and a mark
// saying so would be telling the user what they can already see.
burst-representative: bool,
// TRACES: FR-MRG-6
// Where the cell sits in the grid's lattice of slots — row
// `slot / columns`, column `slot % columns` — and how many slots it
// takes: one, or two or three for a panorama, or the whole row. Rust's
// layout (`library_ui::layout`) is the one place these are computed.
slot: int,
span: int,
}
// TRACES: FR-CAT-3 | FR-CAT-4 | FR-CAT-5 | FR-CAT-6 | FR-CAT-7 | FR-CULL-4 | FR-CULL-5
@@ -535,8 +542,14 @@ export global Library {
callback library-pinch-cells(float);
/// A pinch on the grid has begun, so the press that opened it was not one.
callback library-pinch-started();
/// The slot the grid's first row should show — a slot, not an ordinal:
/// see `LibraryCell.slot`.
in property <int> library-scroll-to: 0;
in property <int> library-scroll-token: 0;
/// TRACES: FR-MRG-6
/// The slots the whole list takes, which a panorama's cell makes more
/// than its count of photographs: what the scrollbar spans.
in property <int> library-total-slots: 0;
/// Which row of the loaded window is the photograph currently open in
/// develop, so the roll can mark it. `-1` when it is not in the window.
/// Rust re-finds it on every reload, so it follows the photograph and not
@@ -559,6 +572,7 @@ export global Library {
/// what the roll happens to hold.
callback library-roll-step(int);
callback library-columns-changed();
/// The first visible row's first slot.
callback library-scrolled(int);
/// How many cells the grid's viewport shows at once. Rust sizes the
/// loaded window from it — see `library_ui::window_move`.
@@ -616,7 +630,15 @@ export global Library {
// ordinal. Rust owns it — clamping it needs the library's length, and
// moving it may have to swap the loaded window underneath.
in property <int> library-cursor: -1;
/// The slot the cursor's photograph starts at, for keeping its row in
/// view.
in property <int> library-cursor-slot: -1;
callback library-move-cursor(int, bool);
/// TRACES: FR-MRG-6
/// Move the cursor by rows, which with a panorama's cell in the way is
/// not a fixed number of photographs: Rust asks the layout what is under
/// the cursor that many rows on.
callback library-move-cursor-rows(int, bool);
callback library-open-cursor();
/// TRACES: FR-CAT-5 | FR-CAT-6
/// Keywording the grid's selection. The catalog has been searchable by
@@ -1461,6 +1483,10 @@ export component LibraryGrid inherits Rectangle {
in property <[LibraryCell]> cells;
in property <int> total: 0;
/// TRACES: FR-MRG-6
/// The slots the whole list takes: `total` where every cell is one slot,
/// more where a panorama spans several or a row ends short of one.
in property <int> total-slots: 0;
in property <bool> scanning: false;
/// The catalog on this device is being opened and checked before the scan
/// starts. Distinct from `scanning` for the reason the empty state below
@@ -2036,6 +2062,9 @@ export component LibraryGrid inherits Rectangle {
// and the first arrow press picks up where the view already is rather than
// teleporting to image zero.
in property <int> cursor: -1;
/// The slot the cursor's photograph starts at; its row is what is kept
/// in view.
in property <int> cursor-slot: -1;
/// Move the cursor by a number of images; the flag extends the selection
/// from the anchor instead of replacing it.
///
@@ -2043,6 +2072,10 @@ export component LibraryGrid inherits Rectangle {
/// longer than the library, which Rust clamps — so this file needs to know
/// neither how many images there are nor where the loaded window starts.
callback move-cursor(int, bool);
/// Move the cursor by rows. Up and down are not a row's worth of
/// photographs once a panorama spans columns, so Rust finds the one
/// under the cursor that many rows on.
callback move-cursor-rows(int, bool);
/// Open the image under the cursor. `Return`, and the reason the arrows
/// are worth having: a cull is walk, judge, open, back, without the hand
/// ever leaving the keyboard.
@@ -2205,7 +2238,6 @@ export component LibraryGrid inherits Rectangle {
// start-up. Every month then began a row and was announced wherever
// its first cell fell, mid-row included.
init => { root.columns-changed(root.columns); }
property <int> row-count: ceil(root.cells.length / max(1, columns));
// --- while a pinch is happening, and just after -----------------------
//
@@ -2276,7 +2308,8 @@ export component LibraryGrid inherits Rectangle {
property <int> viewport-cells: root.columns * (root.visible-rows + 1);
changed viewport-cells => { root.viewport-cells-changed(root.viewport-cells); }
/// Rows the *whole library* occupies, which is what the scrollbar spans.
property <int> total-rows: ceil(root.total / max(1, columns));
/// Counted in slots, since a panorama's cell takes several.
property <int> total-rows: ceil(max(root.total, root.total-slots) / max(1, columns));
background: Theme.ground;
@@ -3437,21 +3470,19 @@ export component LibraryGrid inherits Rectangle {
return accept;
}
if (Keys.chord(event) == "Up" || Keys.chord(event) == "Shift+Up") {
root.move-cursor(-root.columns, event.modifiers.shift);
root.move-cursor-rows(-1, event.modifiers.shift);
return accept;
}
if (Keys.chord(event) == "Down" || Keys.chord(event) == "Shift+Down") {
root.move-cursor(root.columns, event.modifiers.shift);
root.move-cursor-rows(1, event.modifiers.shift);
return accept;
}
if (Keys.chord(event) == "PageUp" || Keys.chord(event) == "Shift+PageUp") {
root.move-cursor(-root.columns * root.visible-rows,
event.modifiers.shift);
root.move-cursor-rows(-root.visible-rows, event.modifiers.shift);
return accept;
}
if (Keys.chord(event) == "PageDown" || Keys.chord(event) == "Shift+PageDown") {
root.move-cursor(root.columns * root.visible-rows,
event.modifiers.shift);
root.move-cursor-rows(root.visible-rows, event.modifiers.shift);
return accept;
}
// A step longer than the library, clamped at the far end.
@@ -3603,7 +3634,7 @@ export component LibraryGrid inherits Rectangle {
// on screen moves nothing at all, and one that has just left
// brings in exactly its own row.
property <length> pitch: root.cell-size + Theme.gap;
property <int> cursor-row: floor(root.cursor / max(1, root.columns));
property <int> cursor-row: floor(max(0, root.cursor-slot) / max(1, root.columns));
changed cursor-row => { self.reveal(); }
function reveal() {
@@ -3722,7 +3753,7 @@ export component LibraryGrid inherits Rectangle {
// Sits in the gap above its row, so it labels the row
// rather than displacing it.
y: Theme.gap
+ floor((i + root.offset) / root.columns) * (root.cell-size + Theme.gap)
+ floor(cell.slot / root.columns) * (root.cell-size + Theme.gap)
- 15px;
width: parent.width - 2 * Theme.gap;
text: cell.period-heading;
@@ -3756,10 +3787,12 @@ export component LibraryGrid inherits Rectangle {
// Cells are positioned at their **absolute** place in the
// library, not their index in the loaded window: the window
// starts at `offset`, so a cell drawn at window-index 0 belongs
// wherever `offset` sits in the full grid.
x: Theme.gap + mod(i + root.offset, root.columns) * (root.cell-size + Theme.gap);
y: Theme.gap + floor((i + root.offset) / root.columns) * (root.cell-size + Theme.gap);
width: root.cell-size;
// wherever `offset` sits in the full grid. That place is its
// slot, which Rust computes (FR-MRG-6): a panorama before it
// pushes it along, and a panorama itself spans `span` slots.
x: Theme.gap + mod(cell.slot, root.columns) * (root.cell-size + Theme.gap);
y: Theme.gap + floor(cell.slot / root.columns) * (root.cell-size + Theme.gap);
width: max(1, cell.span) * root.cell-size + (max(1, cell.span) - 1) * Theme.gap;
height: root.cell-size;
// Copy, not move: dropping into a collection files the
@@ -4433,12 +4466,14 @@ export component LibraryGrid inherits Rectangle {
visible: root.held-row >= 0;
x: Theme.gap
+ mod(root.held-row + root.offset, root.columns) * self.pitch
+ mod(root.cells[root.held-row].slot, root.columns) * self.pitch
- self.reach;
y: Theme.gap
+ floor((root.held-row + root.offset) / root.columns) * self.pitch
+ floor(root.cells[root.held-row].slot / root.columns) * self.pitch
- self.reach;
width: root.cell-size + 2 * self.reach;
property <int> span: max(1, root.cells[root.held-row].span);
width: self.span * root.cell-size + (self.span - 1) * Theme.gap
+ 2 * self.reach;
height: root.cell-size + 2 * self.reach;
animate x, y, width, height { duration: 120ms; easing: ease-out; }