Skip to content

Item Layouts

Headers: winui/ui/layouts/Layout.hpp, winui/ui/layouts/StackLayout.hpp, winui/ui/layouts/TileLayout.hpp, winui/ui/layouts/WrapLayout.hpp, winui/ui/layouts/LinedFlowLayout.hpp. Spec: winui/ui/layouts/StackLayout.hpp.

The base of every attachable layout. Not copyable: it belongs to one host.

Derive from NonVirtualizingLayout or VirtualizingLayout, not from this class. A host takes the layout by std::unique_ptr (LayoutPanelControl::set_layout(), ForEachControl::set_layout()) and becomes its owner(). The layout keeps its per-host state in its own members and calls invalidate_measure() when one of its properties changes.

Example:

auto  panel  = std::make_unique<ui::LayoutPanelControl>();
auto* layout = panel->set_layout(std::make_unique<ui::StackLayoutEngine>());
layout->set_spacing(8.0f); // the layout invalidates the panel

ui::StackLayout

Items one after another in a line, like the children of a ui::VerticalStack or ui::HorizontalStack. Builds a ui::StackLayoutEngine.

Example:

window.set_content(ui::ForEach<std::string>{
    .items  = {"One", "Two", "Three"},
    .layout = ui::StackLayout{.orientation = ui::Orientation::Horizontal, .spacing = 8.0f},
    .item   = [](const std::string& text) -> ui::Spec { return ui::Text{.text = text}; },
});

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

ui::StackLayout{
    .orientation, .spacing
}

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

Field Type Default
orientation ui::Orientation ui::Orientation::Vertical The direction items follow each other in.
spacing float 0.0f Gap between adjacent items, DIPs.

build

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

Consumes the value and returns the layout object.

ui::WrapLayout

Items at their own size in a line that wraps, like the children of a ui::WrapPanel. Builds a ui::WrapLayoutEngine.

Example:

window.set_content(ui::ForEach<std::string>{
    .items  = {"news", "sport", "travel"},
    .layout = ui::WrapLayout{.min_item_spacing = 8.0f, .line_spacing = 8.0f},
    .item   = [](const std::string& tag) -> ui::Spec { return ui::Button{.text = tag}; },
});

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

ui::WrapLayout{
    .orientation, .min_item_spacing, .line_spacing, .line_alignment
}

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

Field Type Default
orientation ui::Orientation ui::Orientation::Horizontal The direction of a line.
min_item_spacing float 0.0f Least gap between items on a line, DIPs.
line_spacing float 0.0f Gap between lines, DIPs.
line_alignment ui::ItemsJustification ui::ItemsJustification::Start How a line's leftover space is distributed.

build

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

Consumes the value and returns the layout object.

ui::TileLayout

Equal cells, as many per line as fit, wrapping to the next line. The layout of a ui::TileView. Builds a ui::TileLayoutEngine.

Example:

window.set_content(ui::ForEach<std::string>{
    .items  = {"Ada", "Grace", "Linus", "Margaret"},
    .layout = ui::TileLayout{.min_item_width = 120.0f, .min_row_spacing = 8.0f, .min_column_spacing = 8.0f},
    .item   = [](const std::string& name) -> ui::Spec { return ui::Text{.text = name}; },
});

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

ui::TileLayout{
    .orientation, .min_item_width, .min_item_height, .min_row_spacing, .min_column_spacing,
    .items_justification, .items_stretch, .maximum_rows_or_columns
}

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

Field Type Default
orientation ui::Orientation ui::Orientation::Horizontal The direction of a line of cells.
min_item_width ui::OptionalFloat Cell width, DIPs; unset takes the first item's.
min_item_height ui::OptionalFloat Cell height, DIPs; unset takes the first item's.
min_row_spacing float 0.0f Least gap between rows, DIPs.
min_column_spacing float 0.0f Least gap between columns, DIPs.
items_justification ui::ItemsJustification ui::ItemsJustification::Start How a line's leftover space is distributed between the cells.
items_stretch ui::TileLayoutItemsStretch ui::TileLayoutItemsStretch::None Whether cells grow to use a line's leftover space.
maximum_rows_or_columns std::size_t 0 The most cells on a line; 0 is as many as fit.

build

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

Consumes the value and returns the layout object.

ui::LinedFlowLayout

Justified lines of one height, each item as wide as its aspect ratio asks, like a photo wall. Builds a ui::LinedFlowLayoutEngine.

Example:

ui::Property<std::vector<ui::ImageSource>> photos;

window.set_content(ui::ForEach<ui::ImageSource>{
    .items  = photos,
    .layout = ui::LinedFlowLayout{.line_height = 160.0f, .min_item_spacing = 4.0f, .line_spacing = 4.0f},
    .item   = [](const ui::ImageSource& photo) -> ui::Spec { return ui::Image{.source = photo}; },
});

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

ui::LinedFlowLayout{
    .line_height, .min_item_spacing, .line_spacing, .items_stretch, .items_justification
}

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

Field Type Default
line_height ui::OptionalFloat Height of every line, DIPs; unset takes the first item's.
min_item_spacing float 0.0f Least gap between items on a line, DIPs.
line_spacing float 0.0f Gap between lines, DIPs.
items_stretch ui::LinedFlowLayoutItemsStretch ui::LinedFlowLayoutItemsStretch::None Whether items grow to fill their line.
items_justification ui::ItemsJustification ui::ItemsJustification::Start How a line's leftover space is distributed.

build

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

Consumes the value and returns the layout object.

ui::Layout

The base of every attachable layout. Not copyable: it belongs to one host.

Derive from NonVirtualizingLayout or VirtualizingLayout, not from this class. A host takes the layout by std::unique_ptr (LayoutPanelControl::set_layout(), ForEachControl::set_layout()) and becomes its owner(). The layout keeps its per-host state in its own members and calls invalidate_measure() when one of its properties changes.

Example:

auto  panel  = std::make_unique<ui::LayoutPanelControl>();
auto* layout = panel->set_layout(std::make_unique<ui::StackLayoutEngine>());
layout->set_spacing(8.0f); // the layout invalidates the panel

~Layout

virtual ~Layout()

Layout

Layout(const Layout&) = delete

operator=

Layout& operator=(const Layout&) = delete

owner

LayoutOwner* owner() const

The host this layout is attached to (non-owning), or nullptr.

parallel_measure_safe

virtual bool parallel_measure_safe() const

Whether the host may be measured on a layout worker thread (ParallelLayout) with this layout. Return true only when measure() touches nothing but the layout's own state and the context it is given. It must not realize or recycle an element, read the viewport or invalidate anything. False by default. The built-in layouts virtualize and return false.

invalidate_measure

void invalidate_measure()

Measures and arranges the host again. Call it after a property of the layout changed. Does nothing while detached.

invalidate_arrange

void invalidate_arrange()

Arranges the host again without measuring. Does nothing while detached.

class NonVirtualizingLayout

Derives from ui::Layout.

A layout that measures and arranges every child of the host.

Derive a custom layout for a fixed set of children from it and give it to a LayoutPanel. Each pass sees all children through the context. Keep any state between measure() and arrange() in members.

Example:

// Every child in one column, each as tall as it wants.
class ColumnLayout final : public ui::NonVirtualizingLayout
{
public:
    ui::Size measure(ui::NonVirtualizingLayoutContext& context, ui::Size available) override
    {
        ui::Size total{};
        for (std::size_t i = 0; i < context.item_count(); ++i)
        {
            const ui::Size d = context.measure(i, {available.width, ui::Unbounded});
            total.width = std::max(total.width, d.width);
            total.height += d.height;
        }
        return total;
    }
    ui::Size arrange(ui::NonVirtualizingLayoutContext& context, ui::Size final_size) override
    {
        float y = 0.0f;
        for (std::size_t i = 0; i < context.item_count(); ++i)
        {
            const float h = context.desired_size(i).height;
            context.arrange(i, ui::Rect{0.0f, y, final_size.width, h});
            y += h;
        }
        return final_size;
    }
};

window.set_content(ui::LayoutPanel{
    .layout   = std::make_unique<ColumnLayout>(),
    .content  = {ui::Text{.text = "One"}, ui::Text{.text = "Two"}},
});

measure

virtual Size measure(NonVirtualizingLayoutContext& context, Size available) = 0

Returns the size the children need within available (infinity on an unbounded axis). It must measure every child it will arrange.

arrange

virtual Size arrange(NonVirtualizingLayoutContext& context, Size final_size) = 0

Places every child within final_size and returns the size used.

class VirtualizingLayout

Derives from ui::Layout.

A layout that measures and arranges the part of a collection that is in view.

Derive a custom layout from it and give it to a ForEach, which can then virtualize, or to a LayoutPanel, where every child is in view. measure() realizes only the items inside VirtualizingLayoutContext::realization_rect() and returns the extent of the whole collection. arrange() places the items measure() realized.

Example:

// Fixed-height rows; only the rows in the realization rect are realized.
class RowLayout final : public ui::VirtualizingLayout
{
public:
    ui::Size measure(ui::VirtualizingLayoutContext& context, ui::Size available) override
    {
        const ui::Rect view  = context.realization_rect();
        const std::size_t count = context.item_count();
        first_ = std::min(count, static_cast<std::size_t>(std::max(0.0f, view.y) / Row));
        last_  = std::min(count, static_cast<std::size_t>(std::max(0.0f, view.bottom()) / Row) + 1);
        for (std::size_t i = first_; i < last_; ++i)
        {
            (void)context.measure(context.get_or_create_element_at(i), {available.width, Row});
        }
        return {std::isfinite(available.width) ? available.width : 0.0f, static_cast<float>(count) * Row};
    }
    ui::Size arrange(ui::VirtualizingLayoutContext& context, ui::Size final_size) override
    {
        for (std::size_t i = first_; i < last_; ++i)
        {
            context.arrange(context.get_or_create_element_at(i),
                            ui::Rect{0.0f, static_cast<float>(i) * Row, final_size.width, Row});
        }
        return final_size;
    }

private:
    static constexpr float Row = 32.0f;
    std::size_t            first_ = 0;
    std::size_t            last_  = 0;
};

auto repeater = std::make_unique<ui::ForEachControl>();
repeater->set_layout(std::make_unique<RowLayout>());

initialize_for_context

virtual void initialize_for_context(VirtualizingLayoutContext& context)

Called when a host starts using this layout. uninitialize_for_context() is called when it stops. The default does nothing.

uninitialize_for_context

virtual void uninitialize_for_context(VirtualizingLayoutContext& context)

Called when the host stops using this layout. Drop per-host state here.

measure

virtual Size measure(VirtualizingLayoutContext& context, Size available) = 0

Returns the size of the whole collection laid out within available (infinity on an unbounded axis). Realizes and measures only the items in the context's realization rect.

arrange

virtual Size arrange(VirtualizingLayoutContext& context, Size final_size) = 0

Places the items realized by the last measure() within final_size and returns the size used.

on_items_changed

virtual void on_items_changed(VirtualizingLayoutContext& context, const ItemsChange& change)

Called when the collection changed. The default calls invalidate_measure(), which lays out everything again.

class StackLayoutEngine

Derives from ui::VirtualizingLayout.

A virtualizing layout that places items one after another, spacing apart. A ui::ForEach or a TileView stacks its items with it.

Only the items in or near the viewport are realized. The rest are estimated at the average measured size, so the scroll extent stays stable. Every item gets the full cross extent. It also works in a LayoutPanel, where every child is realized.

Example:

std::vector<std::string> rows = {"Inbox", "Drafts", "Sent"}; // outlives the window

auto  scroll   = std::make_unique<ui::ScrollControl>();
auto* repeater = scroll->set_child(std::make_unique<ui::ForEachControl>());
auto* stack    = repeater->set_layout(std::make_unique<ui::StackLayoutEngine>());
stack->set_orientation(ui::Orientation::Vertical);
stack->set_spacing(8.0f);
repeater->set_item_template([&rows](std::size_t i) { return std::make_unique<ui::TextControl>(rows[i]); });
repeater->set_item_count(rows.size());
window.set_content(std::move(scroll));

StackLayoutEngine

StackLayoutEngine()

A vertical stack with no spacing and virtualization on.

~StackLayoutEngine

~StackLayoutEngine() override

orientation

Orientation orientation() const

The stacking direction; Vertical by default.

set_orientation

void set_orientation(Orientation orientation)

Changes the direction and invalidates the host's measure.

spacing

float spacing() const

The gap between neighboring items along the stack, in DIPs; 0 by default.

set_spacing

void set_spacing(float spacing)

Changes the gap and invalidates the host's measure.

is_virtualization_enabled

bool is_virtualization_enabled() const

Whether only the items in or near the viewport are realized. When off, every item is realized, however few of them show.

set_virtualization_enabled

void set_virtualization_enabled(bool enabled)

Switches virtualization and invalidates the host's measure.

measure

Size measure(VirtualizingLayoutContext& context, Size available) override

Realizes and measures the items the viewport and its cache need, each unbounded along the stack. Returns the estimated extent of all items along the stack and the widest item across.

arrange

Size arrange(VirtualizingLayoutContext& context, Size final_size) override

Places the realized items one after another, each stretched across final_size.

on_items_changed

void on_items_changed(VirtualizingLayoutContext& context, const ItemsChange& change) override

Shifts the realized range to follow an insert, remove or reset of the source.

uninitialize_for_context

void uninitialize_for_context(VirtualizingLayoutContext& context) override

Drops the per-host layout state when a host stops using this layout.

class TileLayoutEngine

Derives from ui::VirtualizingLayout.

A virtualizing layout that puts every item in an equal-sized cell, as many cells per line as fit, line after line.

The cell is exactly min_item_width() x min_item_height() where those are set, else item 0's desired size. Hand it to a ui::ForEach or a TileView through set_layout() or the spec's .layout. Only the lines inside the realization window are realized, but the extent covers every line, so a scroller can jump anywhere. Every setter lays out the host again.

Example:

std::vector<std::string> names = {"Ada", "Grace", "Linus", "Margaret"};

auto grid = std::make_unique<ui::TileLayoutEngine>();
grid->set_min_item_width(120.0f);
grid->set_min_item_height(80.0f);
grid->set_min_row_spacing(8.0f);
grid->set_min_column_spacing(8.0f);
grid->set_items_stretch(ui::TileLayoutItemsStretch::Fill);

window.set_content(ui::Scroll{
    .height  = 400.0f,
    .content = ui::ForEach<std::string>{
        .items      = names,
        .layout     = std::move(grid),
        .virtualize = true,
        .item       = [](const std::string& name) -> ui::Spec { return ui::Text{.text = name}; },
    },
});

TileLayoutEngine

TileLayoutEngine()

Horizontal, cells sized from item 0, no spacing, Start, None, unlimited per line.

~TileLayoutEngine

~TileLayoutEngine() override

orientation

Orientation orientation() const

The direction a line of cells runs in; Horizontal by default.

set_orientation

void set_orientation(Orientation orientation)

Changes the fill direction and lays out the host again.

min_item_width

std::optional<float> min_item_width() const

The cell width in DIPs; nullopt (the default) takes the width from item 0.

set_min_item_width

void set_min_item_width(OptionalFloat width)

Sets or clears the cell width and lays out the host again.

min_item_height

std::optional<float> min_item_height() const

The cell height in DIPs; nullopt (the default) takes it from item 0.

set_min_item_height

void set_min_item_height(OptionalFloat height)

Sets or clears the cell height and lays out the host again.

min_row_spacing

float min_row_spacing() const

The least gap between rows in DIPs; 0 by default. Justification may widen it.

set_min_row_spacing

void set_min_row_spacing(float spacing)

Sets the row gap and lays out the host again.

min_column_spacing

float min_column_spacing() const

The gap between columns in DIPs; 0 by default.

set_min_column_spacing

void set_min_column_spacing(float spacing)

Sets the column gap and lays out the host again.

items_justification

ItemsJustification items_justification() const

How the cells are spread along a line; Start by default.

set_items_justification

void set_items_justification(ItemsJustification justification)

Changes the justification and lays out the host again.

items_stretch

TileLayoutItemsStretch items_stretch() const

Whether cells grow to use the leftover room on a line; None by default. The leftover room is shared out in whole pixels per cell, so a line may still fall a few pixels short.

set_items_stretch

void set_items_stretch(TileLayoutItemsStretch stretch)

Changes the stretch and lays out the host again.

maximum_rows_or_columns

std::size_t maximum_rows_or_columns() const

The most cells per line. 0 (the default) is unlimited.

set_maximum_rows_or_columns

void set_maximum_rows_or_columns(std::size_t max)

Caps the cells per line and lays out the host again.

set_group_sizes

void set_group_sizes(std::vector<std::size_t> sizes)

Lays the indices out in groups: each group is one index for its header, which takes a line of its own across the whole width, then sizes[g] indices in cells, from the start of a new line. The indices run header, its items, the next header, and so on. Every header is as tall as the first one, the cells are sized from the first item, and a line's cells start at its leading edge. An empty vector (the default) lays every index out as a cell. Lays out the host again. A grouped ui::TileView sets it.

group_sizes

const std::vector<std::size_t>& group_sizes() const

The sizes set_group_sizes() was given.

measure

Size measure(VirtualizingLayoutContext& context, Size available) override

Returns the size of the whole collection laid out within available (infinity on an unbounded axis). Realizes and measures only the items in the context's realization rect.

arrange

Size arrange(VirtualizingLayoutContext& context, Size final_size) override

Places the items realized by the last measure() within final_size and returns the size used.

on_items_changed

void on_items_changed(VirtualizingLayoutContext& context, const ItemsChange& change) override

Called when the collection changed. The default calls invalidate_measure(), which lays out everything again.

uninitialize_for_context

void uninitialize_for_context(VirtualizingLayoutContext& context) override

Called when the host stops using this layout. Drop per-host state here.

effective_item_size

Size effective_item_size() const

The cell size of the last measure in DIPs, stretch included. Zero before the first measure.

class WrapLayoutEngine

Derives from ui::VirtualizingLayout.

A virtualizing ForEach layout that places items of any size one after another and wraps to a new line when the next one would not fit. Only the items that show are realized.

Every item keeps its own desired size. Items sit min_item_spacing() apart along a line. Lines are as thick as their largest item and line_spacing() apart, and line_alignment() spreads the items of each line. Lines outside the viewport are not realized. Their extent is estimated from the average of the last hundred measured lines, so the scroll extent can shift slightly as more of the collection is seen. Use TileLayoutEngine when every item is the same size.

Example:

std::vector<std::string> tags = {"news", "sport", "travel", "food", "music"};

auto flow = std::make_unique<ui::WrapLayoutEngine>();
flow->set_min_item_spacing(8.0f);
flow->set_line_spacing(8.0f);
flow->set_line_alignment(ui::ItemsJustification::SpaceBetween);
ui::WrapLayoutEngine* layout = flow.get(); // observer; the repeater owns it

window.set_content(ui::Scroll{
    .height  = 320.0f,
    .content = ui::ForEach<std::string>{
        .items      = tags,
        .layout     = std::move(flow),
        .virtualize = true,
        .item       = [](const std::string& tag) -> ui::Spec { return ui::Button{.text = tag}; },
    },
});

// Later: switch to columns that scroll horizontally.
layout->set_orientation(ui::Orientation::Vertical);

WrapLayoutEngine

WrapLayoutEngine()

Horizontal rows, no spacing, Start alignment.

~WrapLayoutEngine

~WrapLayoutEngine() override

orientation

Orientation orientation() const

The line direction; Horizontal by default.

set_orientation

void set_orientation(Orientation orientation)

Changes the line direction, discards the collected line estimates and measures the host again.

min_item_spacing

float min_item_spacing() const

The least gap between neighbors along a line, in DIPs; 0 by default. The Space* alignments widen it.

set_min_item_spacing

void set_min_item_spacing(float spacing)

Changes the item spacing and measures the host again.

line_spacing

float line_spacing() const

The gap between consecutive lines, in DIPs; 0 by default.

set_line_spacing

void set_line_spacing(float spacing)

Changes the line spacing and measures the host again.

line_alignment

ItemsJustification line_alignment() const

How each line distributes its spare room; ItemsJustification::Start by default.

set_line_alignment

void set_line_alignment(ItemsJustification alignment)

Changes the line alignment and arranges the host again.

measure

Size measure(VirtualizingLayoutContext& context, Size available) override

Realizes and measures the items that fall in the context's realization window, wraps them into lines and returns the estimated size of the whole collection.

arrange

Size arrange(VirtualizingLayoutContext& context, Size final_size) override

Positions the realized items within their lines according to line_alignment().

on_items_changed

void on_items_changed(VirtualizingLayoutContext& context, const ItemsChange& change) override

Updates the realized range and the line estimates after an insert, removal or reset.

uninitialize_for_context

void uninitialize_for_context(VirtualizingLayoutContext& context) override

Drops the realized-line state when the layout leaves its host.

class LinedFlowLayoutEngine

Derives from ui::VirtualizingLayout.

A virtualizing layout that puts items of any width on lines of one fixed height, like a photo wall.

Each item keeps its own aspect ratio. Items are measured at the line height and fill each line greedily. One scale factor per line shrinks its items to fit, and with LinedFlowLayoutItemsStretch::Fill it also widens them to reach the edge. Only the lines in the realization rect are realized. The widths of items never measured are estimated from the average aspect ratio. Host it in a ForEach inside a Scroll, or in a LayoutPanel.

Example:

ui::Property<std::vector<ui::ImageSource>> photos; // filled as the pictures load

auto layout = std::make_unique<ui::LinedFlowLayoutEngine>();
layout->set_line_height(160.0f);
layout->set_line_spacing(4.0f);
layout->set_min_item_spacing(4.0f);
layout->set_items_stretch(ui::LinedFlowLayoutItemsStretch::Fill);

window.set_content(ui::Scroll{
    .height  = 480.0f,
    .content = ui::ForEach<ui::ImageSource>{
        .items      = photos,
        .layout     = std::move(layout),
        .virtualize = true,
        .item       = [](const ui::ImageSource& photo) -> ui::Spec {
            return ui::Image{.source = photo};
        },
    },
});

line_height

std::optional<float> line_height() const

The line height in DIPs; nullopt (the default) takes the height of item 0.

set_line_height

void set_line_height(OptionalFloat height)

Sets the line height and lays out the host again.

actual_line_height

float actual_line_height() const

The line height of the last measure.

line_spacing

float line_spacing() const

The vertical gap between lines, DIPs; 0 by default.

set_line_spacing

void set_line_spacing(float spacing)

Sets the gap between lines and lays out the host again.

min_item_spacing

float min_item_spacing() const

The smallest horizontal gap between items on a line, DIPs; 0 by default. Justification may widen it.

set_min_item_spacing

void set_min_item_spacing(float spacing)

Sets the minimum item gap and lays out the host again.

items_stretch

LinedFlowLayoutItemsStretch items_stretch() const

Whether short lines are stretched to fill; None by default.

set_items_stretch

void set_items_stretch(LinedFlowLayoutItemsStretch stretch)

Sets the stretch mode and lays out the host again.

items_justification

ItemsJustification items_justification() const

How short lines are placed with LinedFlowLayoutItemsStretch::None; Start by default.

set_items_justification

void set_items_justification(ItemsJustification justification)

Sets the justification and lays out the host again.

invalidate_items_info

void invalidate_items_info()

Forgets every measured width and lays out again. Call it when items changed size while the collection stayed the same.

measure

Size measure(VirtualizingLayoutContext& context, Size available) override

Returns the size of the whole collection laid out within available (infinity on an unbounded axis). Realizes and measures only the items in the context's realization rect.

arrange

Size arrange(VirtualizingLayoutContext& context, Size final_size) override

Places the items realized by the last measure() within final_size and returns the size used.

on_items_changed

void on_items_changed(VirtualizingLayoutContext& context, const ItemsChange& change) override

Called when the collection changed. The default calls invalidate_measure(), which lays out everything again.

line_count

std::size_t line_count() const

How many lines the last measure laid out.

class LayoutOwner

What a layout's host does when the layout asks to be redone.

LayoutPanelControl and ForEachControl, the two hosts a Layout attaches to, implement it by measuring or arranging themselves again. A layout reaches it through Layout::invalidate_measure() and Layout::invalidate_arrange().

Example:

// A custom layout: a property change redoes the host only while one is attached.
class GapLayout : public ui::NonVirtualizingLayout
{
public:
    void set_gap(float gap)
    {
        gap_ = gap;
        if (owner() != nullptr)
        {
            invalidate_measure(); // the host's layout_invalidated(true)
        }
    }

private:
    float gap_ = 0.0f;
};

LayoutOwner

LayoutOwner() = default

LayoutOwner

LayoutOwner(const LayoutOwner&) = delete

operator=

LayoutOwner& operator=(const LayoutOwner&) = delete

layout_invalidated

virtual void layout_invalidated(bool measure) = 0

The layout's properties changed. Measure and arrange again when measure is true, else only arrange.

~LayoutOwner

~LayoutOwner() = default

struct ItemsChange

A change to a virtualizing layout's collection, as the host hands it to VirtualizingLayout::on_items_changed().

Hosts build it from their notify_*() calls, such as ForEachControl::notify_inserted(). A layout that caches per-item state uses it to shift or drop that state.

Example:

// A layout that caches each item's height.
class CachedRowLayout : public ui::VirtualizingLayout
{
public:
    void on_items_changed(ui::VirtualizingLayoutContext&, const ui::ItemsChange& change) override
    {
        if (change.action == ui::ItemsChangeAction::Add)
        {
            const auto at = heights_.begin() + static_cast<std::ptrdiff_t>(change.new_index);
            heights_.insert(at, change.new_count, 0.0f);
        }
        else
        {
            heights_.clear();
        }
        invalidate_measure();
    }

private:
    std::vector<float> heights_;
};

Field Type Default
action ItemsChangeAction ItemsChangeAction::Reset The kind of change. The ItemsChangeAction says which index and count fields apply.
old_index std::size_t 0 The first affected item before the change (Remove, Replace, Move).
old_count std::size_t 0 How many items were removed, replaced or moved.
new_index std::size_t 0 The first affected item after the change (Add, Replace, Move).
new_count std::size_t 0 How many items were inserted or replaced in.

class NonVirtualizingLayoutContext

The children of a panel, as a non-virtualizing layout sees them.

The host implements it. A layout calls it only from its measure() and arrange(). Children are addressed by index, in the host's child order, and all of them exist up front.

Example:

class ColumnLayout : public ui::NonVirtualizingLayout
{
public:
    ui::Size measure(ui::NonVirtualizingLayoutContext& context, ui::Size available) override
    {
        ui::Size total{};
        for (std::size_t i = 0; i < context.item_count(); ++i)
        {
            const ui::Size d = context.measure(i, {available.width, ui::Unbounded});
            total.width = std::max(total.width, d.width);
            total.height += d.height;
        }
        return total;
    }
};

~NonVirtualizingLayoutContext

virtual ~NonVirtualizingLayoutContext() = default

item_count

virtual std::size_t item_count() const = 0

How many children the host has.

measure

virtual Size measure(std::size_t index, Size available) = 0

Measures child index in available (infinity on an unbounded axis) and returns its desired size. desired_size() returns the same size until the next measure.

desired_size

virtual Size desired_size(std::size_t index) const = 0

The desired size child index reported at its last measure().

arrange

virtual void arrange(std::size_t index, const Rect& rect) = 0

Places child index, in coordinates relative to the host's content box.

class VirtualizingLayoutContext

A collection, as a virtualizing layout sees it.

The host implements it: ForEachControl, or LayoutPanelControl, where every child counts as realized. A layout calls it only from its measure() and arrange(). It asks only for the items that fall in realization_rect(), measures and arranges them through the context, and lets the host recycle the rest.

Example:

// Realizes and measures the rows, 32 high, that fall in the realization rect.
void measure_rows(ui::VirtualizingLayoutContext& context, ui::Size available)
{
    const ui::Rect view  = context.realization_rect();
    const auto     first = static_cast<std::size_t>(std::max(0.0f, view.y) / 32.0f);
    for (std::size_t i = first; i < context.item_count() && static_cast<float>(i) * 32.0f < view.bottom(); ++i)
    {
        ui::Element& e = context.get_or_create_element_at(i);
        (void)context.measure(e, {available.width, ui::Unbounded});
    }
}

~VirtualizingLayoutContext

virtual ~VirtualizingLayoutContext() = default

item_count

virtual std::size_t item_count() const = 0

How many items the collection holds.

get_or_create_element_at

virtual Element& get_or_create_element_at(std::size_t index, ElementRealizationOptions options) = 0

The element for item index, realized from the host's item template if it is not already. Asking for it keeps it realized through this measure pass. The host owns the element.

get_or_create_element_at

Element& get_or_create_element_at(std::size_t index)

get_or_create_element_at() with ElementRealizationOptions::None.

recycle_element

virtual void recycle_element(Element& element) = 0

Lets the host clear an element now instead of at the end of the pass.

index_of

virtual std::optional<std::size_t> index_of(const Element& element) const = 0

The item an element was realized for; unset when it is not one of this collection's.

realization_rect

virtual Rect realization_rect() const = 0

The part of the layout worth realizing: the viewport plus the cache buffer around it, in layout coordinates. Unbounded (a huge rect) when the host has nothing to scroll it.

visible_rect

virtual Rect visible_rect() const = 0

The viewport alone.

virtual std::optional<std::size_t> recommended_anchor_index() const

The item to lay out first after a long scroll jump, such as a bring-into-view target, if any.

layout_origin

virtual Point layout_origin() const = 0

Where the layout's origin sits in the host's content box. A layout that places items at negative positions moves it.

set_layout_origin

virtual void set_layout_origin(Point origin) = 0

Moves the layout origin; see layout_origin().

measure

virtual Size measure(Element& element, Size available) = 0

Measures an element in available (infinity on an unbounded axis) and returns its desired size, which desired_size() keeps.

desired_size

virtual Size desired_size(const Element& element) const = 0

The desired size element reported at its last measure() through this context.

arrange

virtual void arrange(Element& element, const Rect& rect) = 0

Places an element in the host's content box, in layout coordinates minus layout_origin().

Enumerations

ItemsJustification

enum class ItemsJustification

Where the items of a line go in the room left over along it (a LinedFlowLayout's or TileLayout's ItemsJustification, a WrapLayout's line alignment).

Value
Start Packed at the start (the default).
Center Packed in the middle.
End Packed at the end.
SpaceAround Equal room around each item, so half as much at the ends.
SpaceBetween First and last item at the ends, equal gaps between the rest.
SpaceEvenly Equal gaps between the items and at both ends.

ItemsChangeAction

enum class ItemsChangeAction

What kind of change it was.

Value
Add new_count items inserted at new_index
Remove old_count items removed from old_index
Replace old_count items at old_index replaced by new_count new ones
Move old_count items moved from old_index to new_index
Reset anything else: the whole collection is new

ElementRealizationOptions

enum class ElementRealizationOptions : unsigned

How get_or_create_element_at() hands an element out. Combine with |, test with has_flag().

Value
None Realize it normally; the host recycles it after a pass that did not ask for it.
ForceCreate Realize it even outside the collection's realized range, where the host would otherwise refuse. Use it to measure a representative item.
SuppressAutoRecycle The layout keeps track of it and recycles it itself. The host does not clear it at the end of a pass that did not ask for it.

LinedFlowLayoutItemsStretch

enum class LinedFlowLayoutItemsStretch

Whether lines narrower than the room are widened to fill it.

Value
None lines keep their natural width and are placed by ItemsJustification (the default)
Fill every line but the last is scaled up to run edge to edge

TileLayoutItemsStretch

enum class TileLayoutItemsStretch

Whether cells grow to use the leftover room on a line.

Value
None Cells keep their size (the default).
Fill Cells widen (along the line) so the line fills the available size.
Uniform As Fill, and the other dimension grows in proportion.

Functions

has_flag

bool has_flag(ElementRealizationOptions set, ElementRealizationOptions flag)

Whether set contains flag.

operator|

ElementRealizationOptions operator|(ElementRealizationOptions a, ElementRealizationOptions b)

The union of two option sets.

unbounded_rect

Rect unbounded_rect()

A rect that stands for no limit. A host with nothing to scroll it reports it as its realization rect. It is huge but finite, because infinite bounds would make its right edge NaN.

is_unbounded

bool is_unbounded(const Rect& rect)

Whether a realization rect is unbounded along either axis, as in a non-virtualizing context.

Tests

  • tests/test_layout_panels.cpp
  • tests/test_for_each.cpp