Skip to content

ForEach

Header: winui/ui/collections/ForEach.hpp. Spec: winui/ui/collections/ForEach.hpp.

A repeater: a collection laid out by an attachable Layout that realizes only the items in view.

It has no chrome, selection or scrolling of its own. Put it in a Scroll and it asks its item template only for the items its layout places in the viewport and a cache buffer around it, and destroys the rest as they scroll away. The collection is an index range (set_item_count()) that the template turns into elements. The repeater owns every realized element. Report collection changes with the notify_*() calls so realized elements stay with their items. ListView and TileView are built on it. Use it directly for a custom collection without chrome.

Example:

// Declarative: 500 cards in a wrap layout, inside a scroller.
std::vector<std::string> names;
for (int i = 0; i < 500; ++i)
    names.push_back(std::format("Card {}", i));
window.set_content(ui::Scroll{
    .height  = 320.0f,
    .content = ui::ForEach<std::string>{
        .items      = names,
        .layout     = ui::WrapLayout{},
        .virtualize = true,
        .item       = [](const std::string& name) -> ui::Spec {
            return ui::Text{.text = name, .width = 96.0f, .height = 64.0f};
        },
    },
});

// Imperative:
std::vector<std::string> rows{"First", "Second", "Third"};
auto  scroll   = std::make_unique<ui::ScrollControl>();
auto* repeater = scroll->set_child(std::make_unique<ui::ForEachControl>());
repeater->set_layout(std::make_unique<ui::StackLayoutEngine>())->set_spacing(4.0f);
repeater->set_item_template([&rows](std::size_t i) { return std::make_unique<ui::TextControl>(rows[i]); });
repeater->set_item_count(rows.size());
repeater->on_element_prepared([](const ui::ElementPreparedArgs& args) { std::println("realized {}", args.index); });

rows.insert(rows.begin(), "New first row");
repeater->notify_inserted(0);

ui::ForEach

A repeater: turns a collection into children, and nothing else. Builds a ui::ForEachControl.

.items is the collection, .item is how one of them looks, and .layout is how they are arranged, a vertical stack when left out. It has no selection, keyboard navigation or scrolling of its own. For those, use a view such as ui::ListView, ui::TileView or ui::TableView. To scroll a ForEach, put it in a ui::Scroll. To mix fixed and generated children, put the ForEach in an ordinary stack next to the fixed ones.

By default every item is built and stays built, wherever the ForEach sits. A row keeps its own state, such as a text box's text, for as long as its item is in the collection. Set .virtualize = true for long collections inside a ui::Scroll. Then only the rows in and near view exist, and a row's state is lost when it scrolls away, so keep that state in the item.

.items takes one of three sources, and they differ in how a change reaches the rows:

  • A ui::ObservableVector (.items = todos), for a collection the app edits in place. Each insert builds one row and each removal destroys one. A move keeps the row's element and moves it, and a replacement builds that one row again. .item receives the item as T&. It may keep that reference and bind to the item's own Property members, because an item stays at its address for as long as it is in the collection.
  • A ui::Property<std::vector<T>> (.items = photos), for a collection the app assigns as a whole, for example after loading, filtering or sorting it. With .key, the old and the new keys are compared. A row whose key is still there keeps its element and state wherever it moved to, and is built again only when its item compares unequal. A change that needs very many moves, such as a shuffle of several thousand items, arrives as one reset instead, which builds every row again. Without .key, the rows before and after the edit are kept and those in between are built again. That covers appending, inserting or removing a run of items, but not a reorder. The rows read a copy of the collection the control owns, so .item must not keep the reference it is given.
  • A plain std::vector<T>, copied once, for a fixed collection.

Name the item type, as in ui::ForEach<Photo>. C++ deduces a spec's type only from the field's own type, which a Property or an ObservableVector is not.

Lifetime: .item is stored on the built control and called for the life of the tree, so it must not capture anything shorter-lived than the window.

Example:

struct Photo
{
    int         id = 0;
    std::string title;
    friend bool operator==(const Photo&, const Photo&) = default;
};
ui::Property<std::vector<Photo>> photos{{{1, "Lake"}, {2, "Bridge"}, {3, "Harbor"}}};

window.set_content(ui::Scroll{.content = ui::ForEach<Photo>{
    .items  = photos,
    .key    = &Photo::id,
    .layout = ui::TileLayout{.min_item_width = 160.0f, .min_row_spacing = 8.0f, .min_column_spacing = 8.0f},
    .item   = [](const Photo& p) -> ui::Spec { return ui::Text{.text = p.title}; },
}});

std::vector<Photo> sorted = photos.get();
std::ranges::sort(sorted, {}, &Photo::title);
photos = sorted; // the rows move, none is built again

Every field, in the order a designated initializer names them:

ui::ForEach{
    .items, .key, .layout, .virtualize, .horizontal_cache_length, .vertical_cache_length,
    .item_transitions, .background, .style, .margin, .width, .height, .min_width, .min_height,
    .max_width, .max_height, .horizontal_alignment, .vertical_alignment, .visible, .tool_tip,
    .accessibility, .on_element_prepared, .on_element_clearing, .on_element_index_changed,
    .on_pointer_entered, .on_pointer_exited, .on_got_focus, .on_lost_focus, .on_key_down,
    .on_wheel, .control, .item
}

Fields, in declaration order (designators must follow it):

Field Type Default
items ui::Binding<std::vector<T>> The collection. A ui::ObservableVector or a ui::Property<std::vector<T>> is followed as it changes. A plain vector is copied once.
key ui::ItemKey<std::type_identity_t<T>> What identifies an item when .items is bound to a ui::Property<std::vector<T>>: a member pointer or a callable returning the key. With it, a row follows its item through any reorder. Keys must be unique. A collection that repeats one is rebuilt as a whole, and a debug build asserts. Not needed with a ui::ObservableVector.
layout ui::LayoutSpec How the items are arranged; a vertical stack when left out.
virtualize bool false When false, every item is built and keeps its state. When true, only the rows in and near view are built, for a long collection inside a ui::Scroll. A row's state is then lost when it scrolls away.
horizontal_cache_length ui::Binding<float> {2.0f} With virtualize: how many viewports' worth of rows to keep built beyond the one in view, split evenly before and after it. 2, one on each side, by default; 0 builds only what shows.
vertical_cache_length ui::Binding<float> {2.0f} See horizontal_cache_length.
item_transitions ui::TransitionList Transitions every row runs. With a ui::AddRemoveTransition an inserted row grows in, a removed one shrinks away and the rows after it slide into place.
background ui::Binding<ui::Paint> Background behind the rows: a fixed color, or a theme color that follows theme switches. Unset draws none.
on_element_prepared ui::Callback<void(const ui::ElementPreparedArgs&)> A row was built for the item at this position, before it is measured. With virtualize that happens as rows scroll into view.
on_element_clearing ui::Callback<void(ui::Element&)> A row is about to be destroyed because it scrolled out, its item was removed, or the collection was reset.
on_element_index_changed ui::Callback<void(const ui::ElementIndexChangedArgs&)> A change of the collection moved a row's item, with the old and the new position.
on_pointer_entered ui::Action The pointer moved onto the control.
on_pointer_exited ui::Action The pointer left the control.
on_got_focus ui::Action Keyboard focus arrived on the control, or on anything inside it, from outside it. A control with nothing focusable in it never gets focus, and never raises this.
on_lost_focus ui::Action Keyboard focus left the control and everything inside it.
on_key_down ui::Callback<ui::Handling(const ui::KeyEvent&)> A key pressed while focus is on the control or inside it, before the control's own handling; ui::Handled stops it there (Element::on_key_down()).
on_wheel ui::Callback<ui::Handling(const ui::PointerEvent&)> A wheel turn over the control, before the control scrolls; ui::Handled stops it there (Element::on_wheel()).
control ui::ControlOut<std::type_identity_t<ui::ForEach>> Out: the handle that receives the built control. Its type uses std::type_identity_t, so passing a handle does not take part in deducing the type argument.
item ui::Callback<ui::Spec(std::type_identity_t<T>&)> How one item looks: a callable taking const T&, or T& to edit an item of a ui::ObservableVector. Its type uses std::type_identity_t, so .items alone decides T when the type argument is left out.

Plus the common fields style, margin, width, height, min_width, min_height, max_width, max_height, horizontal_alignment, vertical_alignment, visible, tool_tip, accessibility every spec carries (conventions).

build

std::unique_ptr<Element> build()&&

Consumes the spec and returns the built ui::ForEachControl; the caller owns it.

ui::ForEachControl

Derives from ui::Element, ui::LayoutOwner.

A repeater: a collection laid out by an attachable Layout that realizes only the items in view.

It has no chrome, selection or scrolling of its own. Put it in a Scroll and it asks its item template only for the items its layout places in the viewport and a cache buffer around it, and destroys the rest as they scroll away. The collection is an index range (set_item_count()) that the template turns into elements. The repeater owns every realized element. Report collection changes with the notify_*() calls so realized elements stay with their items. ListView and TileView are built on it. Use it directly for a custom collection without chrome.

Example:

// Declarative: 500 cards in a wrap layout, inside a scroller.
std::vector<std::string> names;
for (int i = 0; i < 500; ++i)
    names.push_back(std::format("Card {}", i));
window.set_content(ui::Scroll{
    .height  = 320.0f,
    .content = ui::ForEach<std::string>{
        .items      = names,
        .layout     = ui::WrapLayout{},
        .virtualize = true,
        .item       = [](const std::string& name) -> ui::Spec {
            return ui::Text{.text = name, .width = 96.0f, .height = 64.0f};
        },
    },
});

// Imperative:
std::vector<std::string> rows{"First", "Second", "Third"};
auto  scroll   = std::make_unique<ui::ScrollControl>();
auto* repeater = scroll->set_child(std::make_unique<ui::ForEachControl>());
repeater->set_layout(std::make_unique<ui::StackLayoutEngine>())->set_spacing(4.0f);
repeater->set_item_template([&rows](std::size_t i) { return std::make_unique<ui::TextControl>(rows[i]); });
repeater->set_item_count(rows.size());
repeater->on_element_prepared([](const ui::ElementPreparedArgs& args) { std::println("realized {}", args.index); });

rows.insert(rows.begin(), "New first row");
repeater->notify_inserted(0);

ItemTemplate

using ui::ForEachControl::ItemTemplate = Callback<std::unique_ptr<Element>(std::size_t index)>

Makes the element for item index and may choose a different look per item. Called when the item is realized. The repeater owns what it returns.

ForEachControl

ForEachControl()

An empty repeater with a default StackLayoutEngine and cache lengths of 2.

~ForEachControl

~ForEachControl() override

ForEachControl

ForEachControl(const ForEachControl&) = delete

operator=

ForEachControl& operator=(const ForEachControl&) = delete

set_item_template

void set_item_template(ItemTemplate item_template)

Replaces the template. Every realized element is cleared and made again.

has_item_template

bool has_item_template() const

Whether an item template is set. Without one nothing is realized.

item_count

std::size_t item_count() const

How many items the collection holds.

set_item_count

void set_item_count(std::size_t count)

Replaces the collection with count new items (a reset).

notify_inserted

void notify_inserted(std::size_t index, std::size_t count = 1)

Reports that count items were inserted at index.

notify_removed

void notify_removed(std::size_t index, std::size_t count = 1)

Reports that count items were removed from index. With an AddRemoveTransition among the children_transitions(), each removed element fades out in place before it is destroyed.

notify_replaced

void notify_replaced(std::size_t index, std::size_t count = 1)

Reports that count items at index were replaced. Their elements are made again.

notify_moved

void notify_moved(std::size_t from, std::size_t to)

Reports that one item moved from from to to, its position after the move. Its element moves with it.

notify_reset

void notify_reset()

Reports any other change: the whole collection is new, with the same count.

set_layout

template <typename T>
T* set_layout(std::unique_ptr<T> layout)

Takes ownership of the layout and returns an observer. Replaces the previous layout and clears every realized element. nullptr restores the default StackLayoutEngine.

layout

Layout* layout() const

The current layout, non-owning and never nullptr.

set_background

void set_background(std::optional<Color> color)

Background behind the items, as a fixed color; nullopt (the default) draws none. Redraws.

set_background

void set_background(ThemeColor resource)

Background as a theme color, read again when the theme changes. Redraws.

horizontal_cache_length

float horizontal_cache_length() const

How many viewports' worth of items to keep realized beyond the one in view, split evenly before and after it. 2 by default, one on each side. 0 realizes only what shows.

set_horizontal_cache_length

void set_horizontal_cache_length(float length)

Sets the horizontal cache length and lays the repeater out again.

vertical_cache_length

float vertical_cache_length() const

The vertical counterpart of horizontal_cache_length().

set_vertical_cache_length

void set_vertical_cache_length(float length)

Sets the vertical cache length and lays the repeater out again.

try_get_element

Element* try_get_element(std::size_t index) const

The realized element for item index (non-owning), or nullptr.

element_index

std::optional<std::size_t> element_index(const Element& element) const

The item a realized element stands for. Unset when element is not one of the repeater's realized elements.

get_or_create_element

Element* get_or_create_element(std::size_t index)

Realizes item index now, laid out where the layout puts it. The repeater owns the element. Returns nullptr only for an index out of range or a repeater with no template.

realized_count

std::size_t realized_count() const

How many items are realized.

realized_indices

std::vector<std::size_t> realized_indices() const

The realized items' indices, ascending.

on_element_prepared

void on_element_prepared(Callback<void(const ElementPreparedArgs&)> handler)

An element was made for item index, before it is measured.

on_element_clearing

void on_element_clearing(Callback<void(Element&)> handler)

An element is about to be destroyed because it scrolled out, its item was removed, or the collection was reset.

on_element_index_changed

void on_element_index_changed(Callback<void(const ElementIndexChangedArgs&)> handler)

A collection change moved an element's item, with the old and the new index.

layout_extent

Rect layout_extent() const

The layout's extent in layout coordinates, covering every item, realized or estimated.

realization_rect

Rect realization_rect() const

The realization rect of the last layout pass, in layout coordinates.

for_each_child

void for_each_child(ChildVisitor visit) const override

The realized elements, in item order. An item scrolled out of the realization rect has no element to list.

automation_children

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

Only the realized elements that have been arranged. One realized but not yet placed has no usable bounds.

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().

tick_override

void tick_override(Duration delta) override

Advances the background and exit transitions, then ticks a snapshot of the realized elements. A child's handler may change the collection. An element recycled or cleared on the way is skipped.

struct ElementPreparedArgs

What on_element_prepared gets: an element about to show item index.

Field Type Default
element Element& The realized element.
index std::size_t The item it shows.

struct ElementIndexChangedArgs

What on_element_index_changed gets: an element whose item moved.

Field Type Default
element Element& The realized element.
old_index std::size_t Where its item was.
new_index std::size_t Where its item is now.

Tests

  • tests/test_for_each.cpp
  • tests/test_dynamic.cpp