Build the People rail rows you can see, not the ones you cannot

`Flickable { VerticalLayout { for ... } }` instantiates every row. On the
reference library that is 14,268 row subtrees, which measures at 4.2
seconds before a pixel is drawn — and it was paid again on every action,
because every action reloads the model. Roughly half the wait between
clicking "Identity" and the screen appearing was this.

Slint's compiler has a virtualising path for a `for`; it is what makes
`std-widgets`' `ListView` cheap. It keys on the parent element's base
being *named* `ListView` and exposing the five lengths its layouting code
writes back, and a custom base is explicitly allowed. So `widgets.slint`
grows one, and `std-widgets` stays out of the file that establishes our
style. Measured on the real screen: 200,000 rail rows now render in 62ms.

Three things had to be true together, and each was silent on its own.

**The row height must be constant.** The rail hid a set-aside person with
`height: cond ? 52px : 0px`, a height that reads the model — so the
layout cannot place row N without building rows 0..N, and Slint builds
them all. The filtering moves to Rust, where the toggle was already
reloading anyway.

**The list must not be wrapped.** It carries its own stretch and preferred
size, having no natural height to offer; a Rectangle in between hands the
layout that Rectangle's constraints, which are taken from the list and
are therefore nothing.

**The panel must let it fill.** `Panel` lays its children out with
`alignment: start`, which gives each its preferred height — right for a
column of sliders, wrong for anything that scrolls. Hence `Panel.fill`.

Get any of them wrong and the rail renders empty, with no error and a
model full of people. All three were, in turn, before a headless render
of the real screen showed a blank rail.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-30 18:30:16 +02:00
co-authored by Claude Opus 5
parent f26f1ab694
commit d7b851f622
3 changed files with 123 additions and 14 deletions
+32 -13
View File
@@ -15,7 +15,7 @@
// the confirm/reject pair sits on the face itself rather than behind a menu.
import { Theme } from "theme.slint";
import { Panel, Button, IconButton, Field } from "widgets.slint";
import { Panel, Button, IconButton, Field, ListView } from "widgets.slint";
import { SliderRow } from "controls.slint";
import { Icon } from "icons.slint";
@@ -318,6 +318,9 @@ export component IdentityScreen inherits Rectangle {
// than disappearing — 132px keeps the 40px cover and elides the
// labels, and leaves the faces grid enough width for two columns.
width: root.compact ? 132px : 260px;
// The rail scrolls, so its column has to be given the panel's
// height rather than the sum of its parts — see `Panel.fill`.
fill: true;
VerticalLayout {
padding: Theme.gap;
@@ -346,18 +349,30 @@ export component IdentityScreen inherits Rectangle {
wrap: word-wrap;
}
Flickable {
VerticalLayout {
spacing: 2px;
alignment: start;
// A `ListView`, so the rail costs what is on screen rather than
// what the library holds. See `widgets.slint` — and heed its
// one rule: **the row height below is a constant**, and the
// model arrives already filtered. Hiding a row by collapsing it
// to 0px, which is what this did, is a height that reads the
// model, and it makes Slint build all of them anyway.
//
// A direct child of this layout, with nothing wrapped around
// it. `ListView` carries its own stretch and preferred size —
// it has to, having no natural height to offer — and putting a
// plain Rectangle in between hands the layout that Rectangle's
// constraints instead, which are taken from the list and are
// therefore nothing. The rail comes out empty, with every
// person still in the model.
ListView {
for p[i] in root.people: Rectangle {
// 52 of a row, then the 2px that used to be the
// layout's `spacing`. Virtualisation places row N
// at N × this, so the gap lives inside the row.
height: 54px;
for p[i] in root.people: Rectangle {
// Height and visibility rather than filtering the
// model in Rust: the rail is rebuilt on every
// action, and re-deriving a second filtered list
// per rebuild is work the toggle can do for free.
visible: root.show-ignored || !p.ignored;
height: (root.show-ignored || !p.ignored) ? 52px : 0px;
Rectangle {
y: 0px;
height: 52px;
border-radius: Theme.radius;
background: p.id == root.selected-person
? Theme.selected
@@ -408,7 +423,11 @@ export component IdentityScreen inherits Rectangle {
}
}
Rectangle { }
// No spacer between the rail and the buttons below it. There
// used to be one, to push them to the bottom of a short rail;
// the rail now stretches into that space itself, and a second
// stretching child would halve the list to make room for
// nothing.
// What the "not interested" button has hidden, and the way
// back to it. A count with no way to reveal it would make the