Add thumbnail size classes and grid zoom; fix the scrub ordinal
The grid now zooms, which needs thumbnails at two resolutions rather than one, and exposed a scrub that landed in the wrong place. **Two thumbnail size classes.** `ThumbSize::Grid` (256px, ~10 KB) and `Large` (1024px, ~45 KB), with the class part of the store key so both coexist. Storing everything large would take the reference library from ~200 MB to ~860 MB, and shards sync, so that is transfer cost on every device rather than only disk. A store written before the class existed migrates in place: its entries are all grid-sized, which is what the column defaults to, so nothing already fetched is discarded. `forget` now drops every size for an image. Reading a single row left the other class's bytes on the shard's tally for good, sealing it early on space nothing occupied. **Grid zoom.** Ctrl+wheel and pinch resize cells between 90px and 420px in geometric steps, so the gesture feels the same at either end where a fixed pixel step would be imperceptible at 400px and violent at 90px. Crossing 256px switches to the large class, so a zoomed cell is sharp rather than upscaled. Columns and window capacity already derived from cell size, so the grid reflows for free. **The scrub landed about half a library too high.** It counted only dated images while the grid shows all of them — 10,733 dated against 19,841 rows — and ignored `shadowed_by`. Verified against the live catalog: the old formula gave 10,887, the new one 10,732, the true grid position 10,732. The scrub's count and the grid's window must use identical predicates and ordering; a test now fails if they diverge. **Timeline gestures are continuous.** Scrub and pan were quantised to whole buckets, so a slow drag did nothing until it crossed a boundary and then jumped a month. Both work in fractions of the visible span now, and pinch-to-zoom arrives for tablet, where there is no wheel to reach the axis with. The pinch accumulator was wrong on first writing: it took at most one step per update, so an 8x spread — three doublings — yielded one zoom level. `log2().trunc()` now extracts every whole doubling and carries the remainder. The original test asserted the wrong number and defended it in a comment, which is worth remembering: a test can entrench a bug as readily as catch one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -510,9 +510,16 @@ export component LibraryGrid inherits Rectangle {
|
||||
/// rests at the middle rather than implying a choice not yet made.
|
||||
in property <bool> timeline-anchored: false;
|
||||
|
||||
callback scrub(int);
|
||||
/// A fraction along the visible span, not a bucket index — the timeline
|
||||
/// interpolates so a slow drag tracks the finger rather than snapping.
|
||||
callback scrub-fraction(float);
|
||||
callback timeline-pan(float);
|
||||
callback timeline-zoom(int);
|
||||
/// Pinch ratio: above 1 spreads (zoom in), below 1 pinches (zoom out).
|
||||
callback timeline-pinch(float);
|
||||
/// Ctrl+wheel or pinch over the grid: resize the cells. A signed step,
|
||||
/// not a size, so Rust owns the bounds.
|
||||
callback zoom-cells(int);
|
||||
callback columns-changed(int);
|
||||
callback sync-now();
|
||||
/// The grid scrolled: the first visible image's ordinal in the library.
|
||||
@@ -634,7 +641,10 @@ export component LibraryGrid inherits Rectangle {
|
||||
|
||||
// Cell geometry. Columns are derived from the available width so the grid
|
||||
// reflows with the window rather than fixing a count (FR-UI-1).
|
||||
property <length> cell-size: 180px;
|
||||
// Zoomable, so the grid serves both jobs: fewer, larger images for
|
||||
// judging one, and more, smaller ones for finding one. Driven from Rust so
|
||||
// the value survives a scope change and the thumbnail class can follow it.
|
||||
in property <length> cell-size: 180px;
|
||||
property <int> columns: max(1, floor((self.width - Theme.gap) / (cell-size + Theme.gap)));
|
||||
// Reported out so Rust can place month headings: a heading belongs on a
|
||||
// cell that begins a row, and only the grid knows how wide a row is.
|
||||
@@ -999,6 +1009,51 @@ export component LibraryGrid inherits Rectangle {
|
||||
// background still flicks the grid. (This is the part a hand-rolled
|
||||
// TouchArea gesture could not do — see the drag comments above.)
|
||||
if root.total > 0: grid-scroll := Flickable {
|
||||
// Ctrl+wheel resizes the cells; a plain wheel is declined and
|
||||
// falls through to the Flickable's own scrolling. Two jobs on
|
||||
// one gesture, distinguished by the modifier — the convention
|
||||
// every image browser uses.
|
||||
//
|
||||
// Declared *first* so it sits beneath the cells in z-order:
|
||||
// their own touch areas still take clicks and drags, and only
|
||||
// a wheel event nothing else claimed reaches this.
|
||||
zoom-catcher := TouchArea {
|
||||
width: 100%;
|
||||
height: parent.viewport-height;
|
||||
scroll-event(e) => {
|
||||
if (e.modifiers.control) {
|
||||
root.zoom-cells(e.delta-y > 0 ? 1 : -1);
|
||||
return accept;
|
||||
}
|
||||
return reject;
|
||||
}
|
||||
}
|
||||
|
||||
// Two-finger pinch, for tablet: the same gesture the timeline
|
||||
// uses, applied to cell size rather than to time.
|
||||
grid-pinch := ScaleRotateGestureHandler {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
property <float> last-scale: 1.0;
|
||||
|
||||
started => { self.last-scale = 1.0; }
|
||||
updated => {
|
||||
// A quarter-step either way is enough to act on: cell
|
||||
// size is continuous, unlike the timeline's discrete
|
||||
// zoom levels.
|
||||
if (self.scale / max(0.01, self.last-scale) > 1.15) {
|
||||
root.zoom-cells(1);
|
||||
self.last-scale = self.scale;
|
||||
} else if (self.scale / max(0.01, self.last-scale) < 0.87) {
|
||||
root.zoom-cells(-1);
|
||||
self.last-scale = self.scale;
|
||||
}
|
||||
}
|
||||
ended => { self.last-scale = 1.0; }
|
||||
cancelled => { self.last-scale = 1.0; }
|
||||
}
|
||||
|
||||
// Follow a requested position. Without this a scrub moves the
|
||||
// *loaded window* while the viewport stays where it was, so
|
||||
// the cells are drawn thousands of rows away and the grid
|
||||
|
||||
Reference in New Issue
Block a user