Skip to main content

Module list_view

Module list_view 

Source
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 Repeater instead.
  • 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§

ListView
A virtualized scrollable list backed by a ListModel<T> or ListDataSource.
ListViewHandle
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.