Measure the grid against the grid, not the whole view

`cell-size`, `columns` and `visible-rows` were all derived from the
LibraryGrid's own width and height. The cells are not drawn in that box:
the capture-time axis is a sibling of the grid, 96px of it, and the
header takes another 44px off the top.

So `columns` counted the timeline as room for thumbnails and fitted one
more column than there was space for. The last column started inside the
Flickable and ended outside it — clipped, with no sideways scroll to
reach it. At the 180px size class on a phone in portrait the timeline is
a quarter of the screen, which is most of a column.

`visible-rows` was wrong the same way and fed `capacity`, so every window
over-fetched by the ratio of the header to the viewport.

Both now read `grid-area`, the layout the cells actually live in. That is
a descendant, and reading a descendant's geometry is the shape that
causes binding loops elsewhere in this UI — but not here: nothing derived
from these feeds back into the layout. The cells are placed absolutely
inside the Flickable and a Flickable's layout constraints are a bare
`stretch: 1` that its viewport cannot influence.
This commit is contained in:
2026-08-21 20:28:12 +02:00
parent 3cfa78cde2
commit 76a751f125
+25 -4
View File
@@ -933,6 +933,25 @@ export component LibraryGrid inherits Rectangle {
/// below, which is this capped to what the grid can hold. /// below, which is this capped to what the grid can hold.
in property <length> requested-cell-size: 180px; in property <length> requested-cell-size: 180px;
// --- what the grid is measured against ---------------------------------
//
// **`grid-area`, not `self`.** Every quantity below describes the box the
// cells are actually drawn in, and that box is *not* this component: the
// capture-time axis is a sibling of it, 96px wide, and the header sits
// above it. Measuring against `self.width` therefore counted the timeline
// as room for thumbnails and fitted one more column than there was space
// for — the last column ran off the right-hand edge, clipped by the
// Flickable, with no way to scroll sideways to it. At 180px cells that is
// a whole column lost on a phone in portrait, where 96px of 400 is a
// quarter of the screen.
//
// Reading a descendant's geometry is safe here because nothing derived
// from it feeds back into the layout: the cells are placed absolutely
// inside the Flickable, and a Flickable's own layout constraints are a
// bare `stretch: 1` that its viewport cannot influence. That is the loop
// the comments on `panel-visible` in app.slint warn about, and this is not
// an instance of it.
/// The drawn cell, never wider than the column it has to sit in. /// The drawn cell, never wider than the column it has to sit in.
/// ///
/// `columns` floors to a minimum of one, so a cell larger than the grid /// `columns` floors to a minimum of one, so a cell larger than the grid
@@ -946,9 +965,10 @@ export component LibraryGrid inherits Rectangle {
/// choice intact — rotate to landscape, or close the sidebar, and the /// choice intact — rotate to landscape, or close the sidebar, and the
/// thumbnails grow back to the size that was asked for. /// thumbnails grow back to the size that was asked for.
property <length> cell-size: property <length> cell-size:
min(root.requested-cell-size, max(64px, self.width - 2 * Theme.gap)); min(root.requested-cell-size, max(64px, grid-area.width - 2 * Theme.gap));
property <int> columns: max(1, floor((self.width - Theme.gap) / (cell-size + Theme.gap))); property <int> columns:
max(1, floor((grid-area.width - Theme.gap) / (cell-size + Theme.gap)));
// Reported out so Rust can place month headings: a heading belongs on a // 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. // cell that begins a row, and only the grid knows how wide a row is.
changed columns => { root.columns-changed(root.columns); } changed columns => { root.columns-changed(root.columns); }
@@ -960,7 +980,8 @@ export component LibraryGrid inherits Rectangle {
// Derived rather than a constant: a fixed window is simultaneously too // Derived rather than a constant: a fixed window is simultaneously too
// small on a maximised 4K window — where three screenfuls fit inside it — // small on a maximised 4K window — where three screenfuls fit inside it —
// and wasteful on a narrow one. // and wasteful on a narrow one.
property <int> visible-rows: max(1, ceil(self.height / (cell-size + Theme.gap))); property <int> visible-rows:
max(1, ceil(grid-area.height / (cell-size + Theme.gap)));
property <int> capacity: root.columns * (root.visible-rows * 3); property <int> capacity: root.columns * (root.visible-rows * 3);
changed capacity => { root.capacity-changed(root.capacity); } changed capacity => { root.capacity-changed(root.capacity); }
/// Rows the *whole library* occupies, which is what the scrollbar spans. /// Rows the *whole library* occupies, which is what the scrollbar spans.
@@ -1418,7 +1439,7 @@ export component LibraryGrid inherits Rectangle {
zoom(d) => { root.timeline-zoom(d); } zoom(d) => { root.timeline-zoom(d); }
} }
VerticalLayout { grid-area := VerticalLayout {
horizontal-stretch: 1; horizontal-stretch: 1;
// --- empty state -------------------------------------------------- // --- empty state --------------------------------------------------