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..itemreceives the item asT&. 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.itemmust 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::ObservableVectorui::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¶
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¶
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¶
An empty repeater with a default StackLayoutEngine and cache lengths of 2.
~ForEachControl¶
ForEachControl¶
operator=¶
set_item_template¶
Replaces the template. Every realized element is cleared and made again.
has_item_template¶
Whether an item template is set. Without one nothing is realized.
item_count¶
How many items the collection holds.
set_item_count¶
Replaces the collection with count new items (a reset).
notify_inserted¶
Reports that count items were inserted at index.
notify_removed¶
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¶
Reports that count items at index were replaced. Their elements are made again.
notify_moved¶
Reports that one item moved from from to to, its position after the move. Its element moves with it.
notify_reset¶
Reports any other change: the whole collection is new, with the same count.
set_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¶
The current layout, non-owning and never nullptr.
set_background¶
Background behind the items, as a fixed color; nullopt (the default) draws none. Redraws.
set_background¶
Background as a theme color, read again when the theme changes. Redraws.
horizontal_cache_length¶
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¶
Sets the horizontal cache length and lays the repeater out again.
vertical_cache_length¶
The vertical counterpart of horizontal_cache_length().
set_vertical_cache_length¶
Sets the vertical cache length and lays the repeater out again.
try_get_element¶
The realized element for item index (non-owning), or nullptr.
element_index¶
The item a realized element stands for. Unset when element is not one of the repeater's realized elements.
get_or_create_element¶
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¶
How many items are realized.
realized_indices¶
The realized items' indices, ascending.
on_element_prepared¶
An element was made for item index, before it is measured.
on_element_clearing¶
An element is about to be destroyed because it scrolled out, its item was removed, or the collection was reset.
on_element_index_changed¶
A collection change moved an element's item, with the old and the new index.
layout_extent¶
The layout's extent in layout coordinates, covering every item, realized or estimated.
realization_rect¶
The realization rect of the last layout pass, in layout coordinates.
for_each_child¶
The realized elements, in item order. An item scrolled out of the realization rect has no element to list.
automation_children¶
Only the realized elements that have been arranged. One realized but not yet placed has no usable bounds.
automation_role¶
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¶
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¶
Position children within the rect arrange() settled on (also in bounds(), window DIPs). Default: nothing, for a leaf.
render_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¶
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.cpptests/test_dynamic.cpp