Slint — Repeater::ensure_updated_listview (Rust)
A compiled declarative UI where for over a model is a Repeater, and the virtualizing variant differs from the plain one by one method — the same instances, addressed through an offset into the model.
| Language | Rust (with C++, JS, Python bindings) |
| License | GPL-3.0 / commercial / royalty-free |
| Repository | slint-ui/slint (pinned at 24318ce) |
| Documentation | slint.dev · docs/development/model-repeater-system.md |
| Category | Retained (compiled item tree), GPU or software |
| Virtualization unit | Model row ↔ instantiated item tree |
See also Slint in the backend-seam catalog.
Overview
What it solves
In .slint, for item in model: Rectangle { … } compiles to a Repeater that owns a vector of instantiated item trees. Without virtualization the vector is as long as the model. ListView — a ScrollView whose content is a repeater — switches the repeater into a windowed mode.
Design philosophy
The design doc states the model plainly:
For
ListView, only visible items are instantiated:Model rows: [0] [1] [2] [3] [4] [5] [6] [7] [8] [9] ↑ ↑ offset offset + len Instances: [2] [3] [4] [5] [6] (only visible rows instantiated)The
offsettracks which model row corresponds toinstances[0].
An offset plus a dense instance vector — the minimal representation of a realized window, and structurally identical to Avalonia's RealizedStackElements or a ring of rows.
How it works
The repeater's inner state is three fields:
struct RepeaterInner<C: RepeatedItemTree> {
instances: Vec<(RepeatedInstanceState, Option<ItemTreeRc<C>>)>,
offset: usize, // For ListView virtualization
cached_item_height: LogicalLength,
// ...
}— docs/development/model-repeater-system.md
cached_item_height beside offset is the whole extent story: the height of one item, remembered, is what lets the view convert between a scroll offset and a model index without measuring anything else.
The two update entry points differ exactly in whether a viewport is involved:
impl<C: RepeatedItemTree> Repeater<C> {
/// Ensure all instances are up-to-date
pub fn ensure_updated(self: Pin<&Self>, init: impl Fn() -> ItemTreeRc<C>);
/// For ListView with virtualization
pub fn ensure_updated_listview(
self: Pin<&Self>,
init: impl Fn() -> ItemTreeRc<C>,
viewport_width: Pin<&Property<LogicalLength>>,
viewport_height: Pin<&Property<LogicalLength>>,
viewport_y: Pin<&Property<LogicalLength>>,
listview_width: LogicalLength,
listview_height: Pin<&Property<LogicalLength>>,
);
}— docs/development/model-repeater-system.md
Note that ensure_updated_listview writes the viewport properties: it is handed viewport_height and viewport_y as mutable properties because computing the window also determines the virtual extent and may adjust the scroll position (if the model shrank, for instance). The virtualizer owns the scroll geometry; the scroll view merely displays it.
The reused-instance contract is the trait:
pub trait RepeatedItemTree: ItemTree + HasStaticVTable<ItemTreeVTable> + 'static {
type Data: 'static;
/// Called when model data changes
fn update(&self, index: usize, data: Self::Data);
/// Called after first instantiation
fn init(&self) {}
/// For ListView layout
fn listview_layout(self: Pin<&Self>, offset_y: &mut LogicalLength) -> LogicalLength;
}— docs/development/model-repeater-system.md
init is setup, update(index, data) is bind — the GTK 4 split again, here as a compiled trait rather than signals, and generated by the compiler from the .slint source so the application never writes either.
Model changes arrive through a ModelChangeListener that marks is_dirty rather than rebuilding, and listview_geometry_tracker is a dedicated PropertyTracker so that a geometry change (a resize) re-runs the windowing without re-running the model diff.
Analysis
1. What the window bounds
Instantiation of item trees, layout, rendering, and data access through Model::row_data(i) — a real model protocol, implementable over lazily-loaded data.
2. How the range is computed
Division by cached_item_height for the common uniform case, with listview_layout accumulating offset_y per instance for the general one. Slint documents ListView as assuming items of equal height for correct scrollbar behaviour; heterogeneous items work but the extent degrades.
3. What survives between frames
The instances vector (reused across scroll — update rather than re-init), offset, cached_item_height, the model, and the dirty flags.
4. How the extent is known
Declared from cached_item_height × row_count, computed inside ensure_updated_listview and written into viewport_height — the same strategy as egui, reached from a retained architecture.
5. What breaks
- Variable item heights make the extent approximate and scrollbar geometry wrong, which is why the documentation asks for uniform items.
- State inside a repeated element follows the instance, not the row — the universal recycling hazard; Slint mitigates it by generating
updatefrom the declarative bindings, so declared properties are always refreshed and only hand-written state can go stale. - Two update paths (
ensure_updatedvsensure_updated_listview) mean aforinside aListViewbehaves differently from the sameforelsewhere.
Strengths
- The realized window is two fields: an offset and a dense vector.
- Setup/bind is compiler-generated from declarative bindings, so the most common recycling bug is structurally prevented for anything expressed as a binding.
- The virtualizer owning the viewport properties keeps extent and scroll position consistent by construction.
- A dedicated geometry tracker separates "the data changed" from "the viewport changed".
Weaknesses
- Uniform heights assumed for correct scrolling.
- The mechanism is internal: an application cannot supply its own windowing policy, only a model.
- The plain and virtualized repeater paths diverge, so behaviour depends on the enclosing widget.
Key design decisions and trade-offs
| Decision | Rationale | Trade-off |
|---|---|---|
offset + dense instances vector | Minimal realized-window representation; index math is trivial | Only contiguous windows; no sparse realization for e.g. a focused far-away row |
Compiler-generated init / update | The bind half is derived from the declarative bindings, so it cannot be forgotten | Hand-written state in a repeated element is still the author's problem |
cached_item_height | Exact extent and O(1) seek without measuring | Heterogeneous items degrade the scrollbar |
| Virtualizer writes the viewport properties | Extent, scroll position and window stay consistent | The scroll view is not the owner of its own geometry |
Separate listview_geometry_tracker | A resize re-windows without re-diffing the model | Two dirty paths to reason about |