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
+7
View File
@@ -160,9 +160,16 @@ pub fn refresh(
ctl.covers.borrow_mut().retain(|id, _| live.contains(id));
}
// Filtered here rather than by the row hiding itself, which is what the
// rail used to do. A row that collapses to 0px is a height that reads the
// model, and the `ListView` drawing the rail cannot virtualise past one —
// see the rule in `widgets.slint`. The toggle reloads either way, so this
// costs a pass over a list that was already in hand.
let show_ignored = ctl.show_ignored.get();
let rows: Vec<IdentityPerson> = view
.people
.iter()
.filter(|p| show_ignored || !p.ignored)
.map(|p| {
let cover = store.and_then(|store| {
if let Some(img) = ctl.covers.borrow().get(&p.id) {