Expand description
ListView — a virtualized, scrollable list backed by a reactive data model.
ListView<T> materializes widget subtrees only for the rows currently
visible in its viewport (plus a configurable buffer). Scrolling and model
changes trigger a localized rebuild that touches only the newly-visible
slice, leaving the rest of the tree untouched. The data source is a
ListModel<T> (in-memory, reactive) or any ListDataSource<Item = T>
(lazy / external). A delegate closure (index, &T, selected) -> Box<dyn Widget>
produces each row widget on demand.
Row heights come in three modes: uniform (item_height, the 32 dp
default and fastest path), exact callback (item_height_fn — pure,
deterministic per-row sizes), and auto-measured (auto_item_height —
height-for-width measurement with scroll anchoring so content above the
viewport stays put while estimates converge).
§Sections
sections groups the items under full-width header
rows (a song list grouped by artist, a log grouped by day) from the same
provider GridView takes, and
grouping_sections builds one from runs of
equal keys. The headers belong to the scrolled content: they are
virtualized with the rows, counted in the scroll extent, and laid out in all
three height modes, while being exactly
section_header_height tall themselves
(28 dp by default, as in GridView).
pinned_section_headers keeps the
header of the section at the top of the viewport pinned there, until the
next section’s header pushes it out.
A header is not an item. Selection, activation, type-ahead, keyboard
navigation, scroll_to_index and drag and drop all speak indices into the
model, and the keys step over the headers. A drop on a header, or on the
pinned copy of one, inserts at the boundary before its section’s first
item; the insertion line shows on which side of the header the dropped
rows land (see sections).
§Scrolling to an item
handle returns a ListViewHandle, a cheap Clone
taken before the view is added and usable from any handler afterwards. Its
scroll_to_index and
ensure_index_visible are applied at
the next layout pass, after row measurement and scroll anchoring, so they
may follow a model change in the same turn, or be called before mount:
let list = ListView::new(model.clone(), |_i, item, _selected| {
Box::new(TextWidget::new(lit!(item)))
});
let handle = list.handle();
// … `ctx.add(list)`; then, from a handler:
model.insert(0, "Fresh".into());
handle.ensure_index_visible(0);The builder-value methods of the same names compute the offset when they are called, against the last layout pass.
§The keyboard cursor
The list has one current row, its keyboard cursor: the row the arrow keys
step from and the one it names to assistive technology as its active
descendant while it holds keyboard focus. The same ListViewHandle
reads and moves it: cursor,
set_cursor (clamped to the last row;
keyboard focus, the selection and the scroll offset are left alone) and
cursor_signal, for an application to
observe. A cursor set before mount is there at the first build.
In a single selection the cursor follows the selection. In a multiple
selection the two are separate: Ctrl+↓ moves the cursor and leaves
the selection, so after ↓, Ctrl+↓, Ctrl+↓ the selection is still row 0
while the cursor and the active descendant are on row 2, and a selection
written from outside leaves the cursor where it was. Read the cursor, not
the first selected row, for “the row the user is on”.
§Pan to scroll
The view installs common::scrollable::ScrollableBehavior,
which gives it the shared wheel arithmetic, a finger’s pan and the
PanClaim that puts it on a pan’s claimant chain. A pan scrolls it, the
release coasts, and a pan it cannot absorb hands the whole event to the
container outside — never a residual. Vertical only: this view owns no
horizontal offset, so a horizontal pan is declined and chains outward. A pan
that starts on a row scrolls rather than activating it or collapsing a
multi-selection onto it.
§When to use
- Large or dynamically-loaded lists (thousands of rows) — use
ListView. - Small, always-all-visible collections — use
Repeaterinstead. - Hierarchical data — use
TreeView. - Multi-column tabular data — use
TableView.
§Accessibility
The widget is Role::ListBox; each row is wrapped in
Role::ListBoxOption with set_selected state. Those are the interactive
ARIA roles — listbox / option — not the static list / listitem pair,
because this widget has keyboard navigation and selection.
Each row publishes its 1-based position_in_set in the model, and the
container publishes the model’s length as size_of_set, so a screen reader
says “row 147 of 200” rather than counting the realized window. The count
sits on the container because AccessKit resolves an item’s set size by
walking up from it, unlike ARIA’s per-item aria-setsize.
A section header is a Role::Heading named by its section’s title, placed
just before the section’s first realized row in the reading order. It is
not an option, so it takes no position in the set and the rows keep theirs
in the whole model. A header stays realized as long as any row of its
section is, however far above the viewport it has scrolled, so the rows a
screen reader can reach are always read under a heading naming their
section. The pinned copy of a header is hidden from assistive technology,
which reads that in-flow header instead.
The container is the focusable node and rows deliberately are not. The
container names the cursor’s row as its active descendant, which is the row
assistive technology calls current, and each row says through
set_selected whether it is selected. In a single selection the two are one
row: the keys move them together, and a selection the application sets
moves the cursor onto it. The row subtree is kept out of the Tab order, so a
control the delegate puts in a row (the checkbox StandardListItem embeds,
most often) never becomes a Tab stop of its own. Such a control publishes a
keyboard toggle instead, which Space runs. A control in a section header
has no cursor to be reached through, so it is a Tab stop while its header
is on screen. Full
keyboard navigation: arrows, Home, End, PageUp, PageDown (each moving the
selection; in a multiple selection, Ctrl on an arrow or the accelerator on
the others moves only the cursor), Shift for a range and Ctrl+Shift for an
additive one, Space (checks the row when it carries a checkbox, else
select/toggle), Enter (activate), Ctrl+A / Ctrl+Shift+A (select all /
deselect), Ctrl+Arrow and Ctrl+Space (the disjoint-selection pair),
type-ahead (opt-in via type_ahead_label), and Shift+F10 or the Menu key
for the selected row’s context menu. On macOS, Cmd+Down opens the focused
row. The chord table and its rationale are in
docs/data-view-keyboard.md.
let _w = ListView::new(model, |_i, item, _selected| {
Box::new(TextWidget::new(lit!(&item.name)))
})
.item_height(32.0)
.selection(sel);Structs§
- List
View - A virtualized scrollable list backed by a
ListModel<T>orListDataSource. - List
View Handle - A handle on a mounted
ListView, to scroll it to an item from outside the widget — a view-model reacting to a search hit, a “jump to” command.