Skip to content

Design Virtualization

MeowLynxSea edited this page Jun 18, 2026 · 3 revisions

Virtualization

Two factories, two controllers. Both are closure-driven: the caller hands the factory a row closure and the renderer wraps it in gpui::list or gpui::uniform_list.

Factory Controller Wraps Trade-off
virtual_list(id, &controller, cx) VirtualListController gpui::ListState Variable-height rows; supports on_visible_range_change.
uniform_virtual_list(id, count, &controller, cx) UniformVirtualListController gpui::UniformListScrollHandle Same-height rows only; ~10× faster for very large lists. No on_visible_range_change.

The uniform variant does not own the item count — pass it to uniform_virtual_list(...) each frame.

Constructors

use yororen_ui::headless::virtual_list::{UniformVirtualListController, VirtualListController};

let controller = VirtualListController::with_default(0);
// or: VirtualListController::new(count, gpui::ListAlignment::Top, px(16.))

let uniform = UniformVirtualListController::new();

Controller methods (all &self)

Method Purpose
reset(element_count) Replace the total item count (after bulk add/remove).
splice(old_range, count) Replace a range with count new items.
append(n) Append n items at the tail; preserves scroll position. Right call for infinite loading.
scroll_to_reveal_item(ix) Make item ix visible. (UniformVirtualListController exposes scroll_to_item instead.)
scroll_to_top() / scroll_to_bottom() Jump to the ends.

The row closure

FnMut(usize, &mut Window, &mut App) -> AnyElement + 'static. The closure runs inside a row-scoped element namespace, so child ids collide freely across rows.

virtual_list("rows", &controller, cx)
    .row(move |ix, _w, cx| {
        let item = &items[ix];
        list_item(("task-list", item.id), &item.title, cx).render(cx).into_any_element()
    })
    .render(cx)

Infinite loading with on_visible_range_change

virtual_list exposes .on_visible_range_change(F) for lazy load. The renderer hands it to gpui::ListState::set_scroll_handler.

Don't call controller.reset(...) from inside the callback. ListState is Rc<RefCell<…>> and the scroll path already borrows it. Resetting would re-enter the RefCell and panic. Bump a count in your entity instead and call controller.reset(...) at the top of the next render frame.

let entity_for_range = cx.entity().clone();

virtual_list("feed", &controller, cx)
    .row(move |ix, _w, cx| /* per-row element */)
    .on_visible_range_change(move |range, total, _w, cx| {
        entity_for_range.update(cx, |s, _| {
            if range.end + 50 >= total && s.feed_count < 10_000 {
                s.feed_count += 100;
            }
        });
    })
    .render(cx)

When to use which

  • virtual_list — variable-height rows (rows with different content sizes, expand/collapse, async-loaded details).
  • uniform_virtual_list — fixed-height rows (chat bubbles, log lines, settings rows). Significantly faster because gpui::uniform_list measures only the first row.

See also

Clone this wiki locally