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:
@@ -160,9 +160,16 @@ pub fn refresh(
|
|||||||
ctl.covers.borrow_mut().retain(|id, _| live.contains(id));
|
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
|
let rows: Vec<IdentityPerson> = view
|
||||||
.people
|
.people
|
||||||
.iter()
|
.iter()
|
||||||
|
.filter(|p| show_ignored || !p.ignored)
|
||||||
.map(|p| {
|
.map(|p| {
|
||||||
let cover = store.and_then(|store| {
|
let cover = store.and_then(|store| {
|
||||||
if let Some(img) = ctl.covers.borrow().get(&p.id) {
|
if let Some(img) = ctl.covers.borrow().get(&p.id) {
|
||||||
|
|||||||
+32
-13
@@ -15,7 +15,7 @@
|
|||||||
// the confirm/reject pair sits on the face itself rather than behind a menu.
|
// the confirm/reject pair sits on the face itself rather than behind a menu.
|
||||||
|
|
||||||
import { Theme } from "theme.slint";
|
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 { SliderRow } from "controls.slint";
|
||||||
import { Icon } from "icons.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
|
// than disappearing — 132px keeps the 40px cover and elides the
|
||||||
// labels, and leaves the faces grid enough width for two columns.
|
// labels, and leaves the faces grid enough width for two columns.
|
||||||
width: root.compact ? 132px : 260px;
|
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 {
|
VerticalLayout {
|
||||||
padding: Theme.gap;
|
padding: Theme.gap;
|
||||||
@@ -346,18 +349,30 @@ export component IdentityScreen inherits Rectangle {
|
|||||||
wrap: word-wrap;
|
wrap: word-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
Flickable {
|
// A `ListView`, so the rail costs what is on screen rather than
|
||||||
VerticalLayout {
|
// what the library holds. See `widgets.slint` — and heed its
|
||||||
spacing: 2px;
|
// one rule: **the row height below is a constant**, and the
|
||||||
alignment: start;
|
// 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 {
|
Rectangle {
|
||||||
// Height and visibility rather than filtering the
|
y: 0px;
|
||||||
// model in Rust: the rail is rebuilt on every
|
height: 52px;
|
||||||
// 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;
|
|
||||||
border-radius: Theme.radius;
|
border-radius: Theme.radius;
|
||||||
background: p.id == root.selected-person
|
background: p.id == root.selected-person
|
||||||
? Theme.selected
|
? 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
|
// What the "not interested" button has hidden, and the way
|
||||||
// back to it. A count with no way to reveal it would make the
|
// back to it. A count with no way to reveal it would make the
|
||||||
|
|||||||
@@ -676,6 +676,19 @@ export component Panel inherits Rectangle {
|
|||||||
/// `padding` for the layout it may contain, and redeclaring it is a
|
/// `padding` for the layout it may contain, and redeclaring it is a
|
||||||
/// compile error rather than an override.
|
/// compile error rather than an override.
|
||||||
in property <length> inset: Theme.gap;
|
in property <length> inset: Theme.gap;
|
||||||
|
/// Let the contents grow into the panel's full height.
|
||||||
|
///
|
||||||
|
/// The default packs them at the top, which is right for the stacks of
|
||||||
|
/// controls this was written for: a column of sliders should sit under its
|
||||||
|
/// heading, not spread out to meet the bottom edge.
|
||||||
|
///
|
||||||
|
/// **A panel holding something that scrolls needs the other answer**, and
|
||||||
|
/// needs it stated. `alignment: start` gives every child its *preferred*
|
||||||
|
/// height, and a scrolling view has no preferred height worth the name —
|
||||||
|
/// its whole purpose is to be smaller than what it holds. So it is given
|
||||||
|
/// nothing and draws nothing, with no error and no clue: the People rail
|
||||||
|
/// came out blank this way, its model full and its list 0px tall.
|
||||||
|
in property <bool> fill: false;
|
||||||
|
|
||||||
background: Theme.surface;
|
background: Theme.surface;
|
||||||
border-radius: root.flat ? 0px : Theme.radius;
|
border-radius: root.flat ? 0px : Theme.radius;
|
||||||
@@ -685,7 +698,7 @@ export component Panel inherits Rectangle {
|
|||||||
VerticalLayout {
|
VerticalLayout {
|
||||||
padding: root.inset;
|
padding: root.inset;
|
||||||
spacing: root.spacing;
|
spacing: root.spacing;
|
||||||
alignment: start;
|
alignment: root.fill ? LayoutAlignment.stretch : LayoutAlignment.start;
|
||||||
@children
|
@children
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -932,3 +945,73 @@ export component EmptyState inherits VerticalLayout {
|
|||||||
wrap: word-wrap;
|
wrap: word-wrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A scrolling list that only builds the rows you can see.
|
||||||
|
//
|
||||||
|
// # Why this exists rather than `Flickable { VerticalLayout { for … } }`
|
||||||
|
//
|
||||||
|
// That spelling instantiates every row. It is invisible on a list of forty and
|
||||||
|
// it is the whole cost on a list of fourteen thousand: the Identity rail built
|
||||||
|
// 14,268 row subtrees on every rebuild, which measures at 4.2 seconds before a
|
||||||
|
// pixel is drawn — and it paid that again on every action, because every
|
||||||
|
// action reloads the model.
|
||||||
|
//
|
||||||
|
// Slint's compiler has a virtualising path for a `for`, and 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 — see `parent_is_listview` in `i-slint-compiler`'s `object_tree`. A
|
||||||
|
// custom base is explicitly allowed, and that is what this is: the
|
||||||
|
// optimisation without `std-widgets`, whose `ListView` inherits its own
|
||||||
|
// `ScrollView` and would bring a second style into the file that establishes
|
||||||
|
// ours.
|
||||||
|
//
|
||||||
|
// # The rule a caller must keep
|
||||||
|
//
|
||||||
|
// **Every row must be the same, constant height, and that height must not read
|
||||||
|
// the model.** The virtualisation places row N at `N × height` without
|
||||||
|
// building rows 0..N, so a height the model can change is a height the layout
|
||||||
|
// cannot know in advance. Slint's answer to that is to build all of them
|
||||||
|
// anyway — silently, and at the full cost this component exists to avoid. A
|
||||||
|
// row that hides itself with `height: cond ? 52px : 0px` is that mistake
|
||||||
|
// wearing a conditional: to leave a row out, leave it out of the model.
|
||||||
|
//
|
||||||
|
// # Why it wraps a `Flickable` instead of being one
|
||||||
|
//
|
||||||
|
// A `Flickable` takes its preferred *and maximum* height from its viewport, and
|
||||||
|
// the viewport of a virtualising list is written by the layouting pass — which
|
||||||
|
// has not run at the moment the enclosing layout asks how tall this wants to
|
||||||
|
// be. Inheriting `Flickable` therefore answers "nothing", truthfully, and gets
|
||||||
|
// nothing: an empty rail with every person still in the model, which is exactly
|
||||||
|
// what the first attempt at this shipped into a screenshot.
|
||||||
|
//
|
||||||
|
// So the sizing is declared here and the `Flickable` is held inside, filling
|
||||||
|
// it — the same shape, and the same six lines, as `std-widgets`' own
|
||||||
|
// `ScrollView`. Its scrollbars are the part left out: the rails that use this
|
||||||
|
// draw their own chrome, or none.
|
||||||
|
export component ListView {
|
||||||
|
// Aliases rather than bindings, because the list-view layouting writes
|
||||||
|
// through them. The compiler requires all five, as lengths, to recognise
|
||||||
|
// this as a list view at all.
|
||||||
|
out property <length> visible-width <=> flick.width;
|
||||||
|
out property <length> visible-height <=> flick.height;
|
||||||
|
in-out property <length> viewport-width <=> flick.viewport-width;
|
||||||
|
in-out property <length> viewport-height <=> flick.viewport-height;
|
||||||
|
in-out property <length> viewport-x <=> flick.viewport-x;
|
||||||
|
in-out property <length> viewport-y <=> flick.viewport-y;
|
||||||
|
|
||||||
|
callback scrolled <=> flick.flicked;
|
||||||
|
|
||||||
|
min-width: 0px;
|
||||||
|
min-height: 0px;
|
||||||
|
horizontal-stretch: 1;
|
||||||
|
vertical-stretch: 1;
|
||||||
|
preferred-width: 100%;
|
||||||
|
preferred-height: 100%;
|
||||||
|
|
||||||
|
flick := Flickable {
|
||||||
|
width: parent.width;
|
||||||
|
height: parent.height;
|
||||||
|
|
||||||
|
@children
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user