-
Notifications
You must be signed in to change notification settings - Fork 9
Widget VirtualList
A virtualised list. Only the rows visible in the scroll viewport (plus a small overdraw) are mounted, no matter how many items the controller reports.
-
virtual_list— variable row heights. -
uniform_virtual_list— fixed row height (cheaper; one item builder for every row).
- Long scrolling lists (settings pages, search results, file trees, infinite feeds).
- Any UI where you want smooth scrolling and you can structure content as rows.
-
uniform_virtual_listwhen every row is the same height — it measures only the first row, materially faster thangpui::listfor large lists.
Not a good fit:
- A single giant scroll view that is one huge item — virtualization cannot help if one item is huge.
- Variable-height lists where every row also needs a custom
on_visible_range_change— the uniform variant does not expose a scroll handler.
use yororen_ui::headless::virtual_list::{
UniformVirtualListController, VirtualListController,
};
// Variable height — owns the item count.
let controller = VirtualListController::with_default(0);
// Or with explicit alignment + overdraw.
let controller = VirtualListController::new(0, gpui::ListAlignment::Top, px(16.));
// Fixed height — does NOT own the count; the caller passes it each frame.
let controller = UniformVirtualListController::new();| Method | Purpose |
|---|---|
reset(n) |
Inform the list the item count changed to n. Scroll snaps to top. |
splice(old, new) |
Items in old were replaced by new items. |
append(n) |
Append n items at the tail; preserves current scroll. |
scroll_to_reveal_item(ix) / scroll_to_top() / scroll_to_bottom()
|
Scroll helpers. (UniformVirtualListController exposes scroll_to_item instead of scroll_to_reveal_item.) |
fn virtual_list(id, controller: &VirtualListController, cx: &mut App) -> VirtualListProps
fn uniform_virtual_list(id, item_count: usize, controller: &UniformVirtualListController, cx: &mut App) -> UniformVirtualListProps| Method | Purpose |
|---|---|
item_count(n) |
Override the count (otherwise read from the controller on each frame). |
alignment(a) |
ListAlignment::Top / Center / Bottom. virtual_list only. |
overdraw(px) |
Extra pixels above/below the viewport kept mounted. Default 16 px. virtual_list only. |
sizing(b) |
ListSizingBehavior (auto / fixed). |
row(closure) |
Required. FnMut(usize, &mut Window, &mut App) -> AnyElement + 'static. |
on_visible_range_change(closure) |
FnMut(Range<usize>, usize, &mut Window, &mut App) + 'static. virtual_list only. |
render(cx) |
Consume the props; returns Stateful<Div>. |
When the row count changes, notify both the controller and the props. The simplest mental model:
-
Bulk replace →
controller.reset(new_count). Scroll snaps to top. -
In-place edit →
controller.splice(old, new). -
Infinite load (append) →
controller.append(n). Preserves scroll position.
gpui::ListState is Rc<RefCell<…>>. The on_visible_range_change callback fires while the RefCell is already borrowed by paint — calling controller.reset(...) from inside the callback would panic with "RefCell already borrowed". Write to a plain field inside the callback and sync the controller at the top of the next render.
struct FeedView {
list_controller: VirtualListController,
item_count: usize,
selected: Option<usize>,
}
impl Render for FeedView {
fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let current = self.list_controller.state().item_count();
if current < self.item_count {
self.list_controller.append(self.item_count - current);
}
let row_entity = cx.entity().clone();
let range_entity = cx.entity().clone();
virtual_list("feed", &self.list_controller, cx)
.row(move |ix, _w, cx| {
let selected = row_entity.read(cx).selected == Some(ix);
list_item(format!("feed-row-{ix}"), format!("Item {ix}"), cx)
.selected(selected)
.on_click({
let row_entity = row_entity.clone();
move |_, _, cx| row_entity.update(cx, |s, _| s.selected = Some(ix));
})
.render(cx)
.into_any_element()
})
.on_visible_range_change(move |range, total, _w, cx| {
range_entity.update(cx, |s, _| {
if range.end + 50 >= total && s.item_count < 10_000 {
s.item_count += 100;
}
});
})
.render(cx)
.w(px(320.)).h(px(360.))
}
}For the fixed-height variant the count is passed directly to uniform_virtual_list(id, count, &controller, cx) each frame and on_visible_range_change is not available.
- VirtualRow — per-row shell.
- ListItem — the default row content.
- Recipe: search + results list.
- Virtualization design.
Yororen UI v0.3.0 · repository · Apache-2.0 · This wiki documents Yororen UI v0.3.0.
This wiki documents Yororen UI v0.3.0 — the headless-core, swappable-renderer build.