Skip to content

VirtualList

Header: winui/ui/collections/VirtualListControl.hpp.

A list that scrolls itself and creates Elements only for the rows in view.

It asks its item factory for a fresh Element per visible index and drops rows as they scroll out, so a 100,000-item list holds about a dozen live controls. A Scroll, by contrast, keeps its whole child and clips it. The visible rows are created again on every arrange and scroll step, so keep per-row state in the caller's data, not in the row. The list scrolls with the wheel and an overlay scroll bar. TableView's body is one. All rows share item_height() unless set_size_items_to_content() lets each row be as tall as it measures. It has no spec. For a list in a declared tree use ui::ListView. Build this one by hand when rows of one height, an index and a factory are all a list needs.

Example:

std::vector<std::string> names(1000, "Row"); // outlives the window

auto list = std::make_unique<ui::VirtualListControl>();
list->set_item_height(36.0f);
list->set_item_factory([&names](std::size_t i) -> std::unique_ptr<ui::Element> {
    return std::make_unique<ui::TextControl>(names[i]);
});
list->set_item_count(names.size());
ui::VirtualListControl* observer = window.set_content(std::move(list));
auto top_button = std::make_unique<ui::ButtonControl>("Top");
top_button->on_click([observer] { observer->set_scroll_offset(0.0f); }); // back to the top

ui::VirtualListControl

Derives from ui::Element, ui::ScrollProvider.

A list that scrolls itself and creates Elements only for the rows in view.

It asks its item factory for a fresh Element per visible index and drops rows as they scroll out, so a 100,000-item list holds about a dozen live controls. A Scroll, by contrast, keeps its whole child and clips it. The visible rows are created again on every arrange and scroll step, so keep per-row state in the caller's data, not in the row. The list scrolls with the wheel and an overlay scroll bar. TableView's body is one. All rows share item_height() unless set_size_items_to_content() lets each row be as tall as it measures. It has no spec. For a list in a declared tree use ui::ListView. Build this one by hand when rows of one height, an index and a factory are all a list needs.

Example:

std::vector<std::string> names(1000, "Row"); // outlives the window

auto list = std::make_unique<ui::VirtualListControl>();
list->set_item_height(36.0f);
list->set_item_factory([&names](std::size_t i) -> std::unique_ptr<ui::Element> {
    return std::make_unique<ui::TextControl>(names[i]);
});
list->set_item_count(names.size());
ui::VirtualListControl* observer = window.set_content(std::move(list));
auto top_button = std::make_unique<ui::ButtonControl>("Top");
top_button->on_click([observer] { observer->set_scroll_offset(0.0f); }); // back to the top

ItemFactory

using ui::VirtualListControl::ItemFactory = Callback<std::unique_ptr<Element>(std::size_t index)>

Builds the row for item index. The list takes ownership and destroys the row once it scrolls out of view.

VirtualListControl

VirtualListControl() = default

An empty list: no factory, no items, 32-DIP rows.

set_item_factory

void set_item_factory(ItemFactory factory)

Sets the factory that returns the Element for any index in [0, item_count()). It is called for every visible row on each arrange and scroll step, so keep it cheap. Returning nullptr ends the realized rows there, for a source that shrank before set_item_count() caught up. That row and the ones after it stay empty. Replacing the factory rebuilds the rows on the next layout.

item_count

std::size_t item_count() const

How many items the list scrolls over; 0 by default.

set_item_count

void set_item_count(std::size_t count)

Changes the item count and lays the list out again. The scroll offset is clamped again on the next arrange. Forgets the heights the rows measured.

item_height

float item_height() const

Every row's height in DIPs; 32 by default. Each row is arranged to exactly this height, whatever it measures. When the rows size to their content, it is the least height of a row and the estimate for a row not yet seen.

set_item_height

void set_item_height(float height)

Changes the row height, lays the list out again and forgets the heights the rows measured.

size_items_to_content

bool size_items_to_content() const

Whether each row is measured with unbounded height and is as tall as it asks for, at least item_height(). Off by default. A row keeps the height it measured while it is out of view, so scrolling back finds it where it was. Rows never seen count as item_height().

set_size_items_to_content

void set_size_items_to_content(bool on)

Turns content-sized rows on or off and lays the list out again.

forget_item_heights

void forget_item_heights()

Forgets every height the rows measured and lays the list out again. Call it after a change that reflows the rows' content, such as a new column width. The list tracks its own width changes.

item_top

float item_top(std::size_t index) const

Where item index starts, in DIPs from the top of the content, leading space included.

item_extent

float item_extent(std::size_t index) const

Item index's height. That is what it measured while the rows size to their content, and item_height() otherwise.

index_at

std::size_t index_at(float y) const

The item at y DIPs from the top of the content. Returns item_count() past the last item and 0 over the leading space.

leading_space

float leading_space() const

Room before the first item that scrolls with the content, where a ListView puts its header. The items start this far down the content, and the content is this much taller.

set_leading_space

void set_leading_space(float space)

Sets the leading space in DIPs, clamped to >= 0. Lays the list out again only on a change.

content_height

float content_height() const

The content's height: the leading space and every item.

scroll_offset

float scroll_offset() const

How far the content is scrolled, in DIPs from the top (0 is the top).

set_scroll_offset

void set_scroll_offset(float offset)

Sets the offset directly, for example to scroll to the top. Clamps it to the current content range and rebuilds the realized rows at once.

right_gutter

float right_gutter() const

The width the vertical bar takes from the rows, decided by measure(). It is the bar's gutter in eras with a classic bar, such as Aero, while the content overflows. It is 0 for the overlay bar, which draws over the rows.

viewport_rect

Rect viewport_rect() const

Where the rows are shown: the bounds minus the right gutter.

scroll_info

std::optional<ScrollInfo> scroll_info() const override

Element::scroll_info(), the same as a Scroll's. It can be pulled a tenth of its viewport past either end.

first_visible_index

std::size_t first_visible_index() const

The item index of the first realized row. The realized items are [first_visible_index(), first_visible_index() + visible_count()). visible_item() takes an offset into that range. Meant for inspection and testing, not for driving the list from outside.

visible_count

std::size_t visible_count() const

How many rows are realized right now: the rows at least partly in view.

visible_item

Element* visible_item(std::size_t i) const

The ith realized row; 0 is the row of first_visible_index(). Throws std::out_of_range past visible_count(). The list owns the row and destroys it when it scrolls out of view.

for_each_child

void for_each_child(ChildVisitor visit) const override

The realized rows, top first. Only these can take focus. Focus does not scroll a row into view.

automation_children

void automation_children(std::vector<Element*>& out) const override

Reported to screen readers as a scrollable List without children, because only the visible rows exist as Elements.

automation_role

AutomationRole automation_role() const override

What screen readers and other assistive tools see of this node: the role, name, help text and children here, and what a client can do with the node through the providers automation_providers() lists (ui/Automation.hpp).

automation_role() is the role this node reports; Group by default.

measure_override

Size measure_override(Size available, const MeasureContext& context) const override

Implement these instead of measure() and arrange(). The base class has already applied margin, size limits and alignment by the time these run. measure_override() is pure (every concrete Element must size itself); arrange_override() defaults to a no-op since most leaves need nothing beyond having bounds() set, which the base already did.

arrange_override

void arrange_override(const Rect& final_rect, const ArrangeContext& context) override

Position children within the rect arrange() settled on (also in bounds(), window DIPs). Default: nothing, for a leaf.

render_override

void render_override(Renderer& renderer, const RenderContext& context) const override

What the control draws, in layout coordinates; render() has already applied the visual layer. A container draws its children through their render(), never their render_override().

pointer_override

bool pointer_override(const PointerEvent& e) override

A pointer event for this element: the element under the pointer, one inside it that did not handle the event, or the element holding the capture. The position is in layout coordinates (handle_pointer()). Return true when the event did something here, and it goes no further up; a handled press also captures the pointer until its release. A control that acts on a press (a click, a toggle, a caret placed, a drag started) returns true for the press and its release, so a list row or a card around it does not act on the same click. Default: false.

wheel_override

bool wheel_override(const PointerEvent& e) override

A wheel turn over this element (handle_wheel()). Return true when it scrolled something here. Default: an element with no children hands it to pointer_override() and returns false; one with children returns false.

children_hit_clip

std::optional<Rect> children_hit_clip() const override

Where the children can be hit, in this element's layout coordinates, or nullopt for anywhere (the default): a scrolling control returns its viewport, so content scrolled out of view is not hit.

covers_children_at

bool covers_children_at(Point p) const override

Whether a part of this element drawn over its children is at p (a scroll bar over the content), so the element is hit there instead of a child. Default: false.

tick_override

void tick_override(Duration delta) override

Fades the scroll bar, then ticks the rows.

Tests

  • tests/test_layout.cpp