Skip to content

Grid

Header: winui/ui/panels/Grid.hpp. Spec: winui/ui/panels/Grid.hpp.

A two-dimensional layout panel: children placed in cells of row and column tracks, each track Auto, a fraction of the remaining space, or fixed DIPs.

Measure sizes Dip tracks first, then Auto tracks from their single-span children, then splits what is left across the Fraction tracks by weight. Arrange sizes the tracks again for the final rect, so Fraction tracks share the room the Grid is given. Every track stays within its min and max (ui::minmax()). On an unbounded axis a Fraction track behaves like Auto. An axis with no tracks has one implicit Fraction track, so a Grid without definitions layers every child in one cell. A child spanning several Auto tracks does not grow them. A border and padding inset the cells. Tracks can be resized at runtime with set_row_height() and set_column_width(). A Splitter in a track of its own between two others resizes them itself.

Example:

// Declarative: header, a sidebar beside the content, footer.
using ui::AutoSize;
using namespace ui::literals;
window.set_content(ui::Grid{
    .columns        = {200_dip, 1_fr},
    .rows           = {ui::AutoSize, 1_fr, ui::AutoSize},
    .column_spacing = 8.0f,
    .content = {
        ui::Cell{.row = 0, .column = 0, .column_span = 2, .content = ui::Text{.text = "Header"}},
        ui::Cell{.row = 1, .column = 0, .content = ui::Text{.text = "Sidebar"}},
        ui::Cell{.row = 1, .column = 1, .content = ui::Text{.text = "Content"}},
        ui::Cell{.row = 2, .column = 0, .column_span = 2, .content = ui::Text{.text = "Footer"}},
    },
});

// Imperative:
auto grid = std::make_unique<ui::GridControl>();
grid->add_column(ui::GridLength::auto_());
grid->add_column(ui::GridLength::fraction());
grid->add_row(ui::GridLength::auto_());
grid->set_column_spacing(12.0f);
grid->add(std::make_unique<ui::TextControl>("Name:"), 0, 0);
grid->add(std::make_unique<ui::TextBoxControl>(), 0, 1);
window.set_content(std::move(grid));

ui::Grid

A two-dimensional track layout. Builds a ui::GridControl.

Tracks are sized with _dip (fixed), _fr (a fraction of the remaining space) and AutoSize (the content), and ui::minmax() keeps one between two sizes. A track list left empty is one fraction track. A ui::Splitter in a track of its own between two others resizes them. To resize tracks from code, call set_column_width() or set_row_height() on .control.

A deeply nested tree compiles faster with MSVC when a subtree is built into a named ui::Spec first and placed from there.

Example:

using namespace ui::literals;

struct Model
{
    ui::Property<std::string> name;
    ui::Property<std::string> email;
};
Model model;

window.set_content(ui::Grid{
    .columns        = {120_dip, 1_fr},
    .rows           = {ui::AutoSize, ui::AutoSize},
    .row_spacing    = 8,
    .column_spacing = 12,
    .content        = {
        ui::Cell{.row = 0, .column = 0, .content = ui::Text{.text = "Name"}},
        ui::Cell{.row = 0, .column = 1, .content = ui::TextBox{.text = model.name}},
        ui::Cell{.row = 1, .column = 0, .content = ui::Text{.text = "Email"}},
        ui::Cell{.row = 1, .column = 1, .content = ui::TextBox{.text = model.email}},
    },
});

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

ui::Grid{
    .columns, .rows, .row_spacing, .column_spacing, .background, .border_brush,
    .border_thickness, .corner_radius, .padding, .style, .margin, .width, .height, .min_width,
    .min_height, .max_width, .max_height, .horizontal_alignment, .vertical_alignment, .visible,
    .tool_tip, .enabled, .accessibility, .on_pointer_entered, .on_pointer_exited,
    .on_got_focus, .on_lost_focus, .on_key_down, .on_wheel, .control, .content
}

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

Field Type Default
columns std::vector<ui::GridLength> One GridLength per column.
rows std::vector<ui::GridLength> One GridLength per row.
row_spacing ui::Binding<float> {0.0f} Gap between rows, DIPs.
column_spacing ui::Binding<float> {0.0f} Gap between columns, DIPs.
background ui::Binding<ui::Paint> Background behind the cells: a Color, or a ThemeColor read from the theme; empty (the default) draws nothing.
border_brush ui::Binding<ui::Paint> Border color, a Color or a ThemeColor; empty (the default) draws no border.
border_thickness ui::Binding<ui::Thickness> {ui::Thickness{}} Border width per edge, DIPs; it insets the children like padding.
corner_radius ui::Binding<float> {0.0f} Corner radius of the background and the border, DIPs.
padding ui::Binding<ui::Thickness> {ui::Thickness{}} Room between the border and the children, DIPs.
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<ui::Grid> Out: the handle that receives the built control.
content ui::CellList The placed children; see ui::Cell.

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

build

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

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

ui::Cell

One placed child. row/column name tracks declared in .rows/.columns, and either may be bound to move the child as state changes.

Written inline in a ui::Grid's .content list; see ui::Grid for a full example.

Example:

ui::Property<std::size_t> column{0};
ui::Cell moving{.row = 1, .column = column, .column_span = 2, .content = ui::Text{.text = "Here"}};

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

ui::Cell{
    .row, .column, .row_span, .column_span, .content
}

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

Field Type Default
row ui::Binding<std::size_t> {std::size_t{0}} The row index.
column ui::Binding<std::size_t> {std::size_t{0}} The column index.
row_span std::size_t 1 Rows covered.
column_span std::size_t 1 Columns covered.
content ui::Spec The placed child.

ui::CellList

A move-only, already-built list of placed children: the Grid analog of SpecList.

Callers write it as a braced list of ui::Cell{...}; build one in a loop with add().

Example:

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

ui::CellList cells;
for (std::size_t i = 0; i < names.size(); ++i)
{
    cells.add(ui::Cell{.row = i, .content = ui::Text{.text = names[i]}});
}
window.set_content(ui::Grid{.rows    = std::vector(names.size(), ui::AutoSize),
                            .content = std::move(cells)});

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

ui::CellList{
    .cells
}

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

Field Type Default
cells std::vector<Placed> The placed children, in declaration order.

operator=

CellList& operator=(const CellList&) = delete

operator=

CellList& operator=(CellList&&) = default

add

void add(Cell cell)

Builds cell's content and appends it, as one more entry of the braced list would.

struct Placed

One built child with its placement; the node is already owned by the list.

Field Type Default
row Binding<std::size_t> Row index, possibly bound.
column Binding<std::size_t> Column index, possibly bound.
row_span std::size_t Rows covered.
column_span std::size_t Columns covered.
node std::unique_ptr<Element> The built child.

ui::GridControl

Derives from ui::Element.

A two-dimensional layout panel: children placed in cells of row and column tracks, each track Auto, a fraction of the remaining space, or fixed DIPs.

Measure sizes Dip tracks first, then Auto tracks from their single-span children, then splits what is left across the Fraction tracks by weight. Arrange sizes the tracks again for the final rect, so Fraction tracks share the room the Grid is given. Every track stays within its min and max (ui::minmax()). On an unbounded axis a Fraction track behaves like Auto. An axis with no tracks has one implicit Fraction track, so a Grid without definitions layers every child in one cell. A child spanning several Auto tracks does not grow them. A border and padding inset the cells. Tracks can be resized at runtime with set_row_height() and set_column_width(). A Splitter in a track of its own between two others resizes them itself.

Example:

// Declarative: header, a sidebar beside the content, footer.
using ui::AutoSize;
using namespace ui::literals;
window.set_content(ui::Grid{
    .columns        = {200_dip, 1_fr},
    .rows           = {ui::AutoSize, 1_fr, ui::AutoSize},
    .column_spacing = 8.0f,
    .content = {
        ui::Cell{.row = 0, .column = 0, .column_span = 2, .content = ui::Text{.text = "Header"}},
        ui::Cell{.row = 1, .column = 0, .content = ui::Text{.text = "Sidebar"}},
        ui::Cell{.row = 1, .column = 1, .content = ui::Text{.text = "Content"}},
        ui::Cell{.row = 2, .column = 0, .column_span = 2, .content = ui::Text{.text = "Footer"}},
    },
});

// Imperative:
auto grid = std::make_unique<ui::GridControl>();
grid->add_column(ui::GridLength::auto_());
grid->add_column(ui::GridLength::fraction());
grid->add_row(ui::GridLength::auto_());
grid->set_column_spacing(12.0f);
grid->add(std::make_unique<ui::TextControl>("Name:"), 0, 0);
grid->add(std::make_unique<ui::TextBoxControl>(), 0, 1);
window.set_content(std::move(grid));

GridControl

GridControl() = default

An empty grid with no track definitions (one implicit Fraction cell).

add_row

void add_row(GridLength length)

Appends a row track and updates the layout.

add_column

void add_column(GridLength length)

Appends a column track and updates the layout.

row_count

std::size_t row_count() const

How many rows were added (0 means one implicit Fraction row).

column_count

std::size_t column_count() const

How many columns were added (0 means one implicit Fraction column).

row

const GridLength& row(std::size_t index) const

Row track index's definition; throws std::out_of_range past row_count().

column

const GridLength& column(std::size_t index) const

Column track index's definition; throws std::out_of_range past column_count().

set_row_height

void set_row_height(std::size_t index, float dips)

Switches a track to a fixed size in DIPs at runtime, keeping its min and max. A custom Splitter handler drives it from on_drag_delta (ui/panes/Splitter.hpp). An index past the current track count is ignored, so a drag handler need not guard against a grid with fewer tracks.

set_column_width

void set_column_width(std::size_t index, float dips)

The column counterpart of set_row_height().

resize_beside

std::optional<float> resize_beside(const Element* splitter, Orientation orientation, float delta)

Moves delta DIPs from the track after splitter's to the track before it (a negative delta the other way), within both tracks' min and max, so the two keep their total. splitter must be a child of this grid in a track of its own, span 1, with a track on either side: the columns for a Vertical bar, the rows for a Horizontal one. Two Fraction tracks trade weight; any other track becomes a Dip track at its new size. Returns the delta applied after the limits, or nullopt (and nothing changes) when splitter is not placed that way. SplitterControl calls this.

row_size

float row_size(std::size_t index) const

A track's current size in DIPs: the literal value for a Dip track (so it reflects a set_row_height()/set_column_width() call immediately, before any layout pass has run on it), otherwise the size the last layout resolved for it. 0 before the first layout, or for an out-of-range index.

A Splitter or ResizeGripper drag handler must add its delta to this, not to an occupant's bounds(). A child's bounds are its inner rect, already shrunk by its own margin and focus-ring outset (Element::arrange). Feeding child->bounds().width + delta into set_column_width() takes that band off again on every drag event, so the track collapses on the first one and never grows back.

column_size

float column_size(std::size_t index) const

The column counterpart of row_size().

add

template <typename T>
T* add(std::unique_ptr<T> child, std::size_t row, std::size_t column, std::size_t row_span = 1, std::size_t column_span = 1)

Takes ownership of a child and returns a non-owning observer to it, as StackControl::add() does. row/column index the tracks added via add_row()/add_column(); an index past the last track places the child in the last one, and a span stops at the last track. Spans below 1 are raised to 1.

add

template <SpecLike S>
Element* add(S&& spec, std::size_t row, std::size_t column, std::size_t row_span = 1, std::size_t column_span = 1)

The declarative overload: builds spec (or takes the node a ui::Spec holds) and adds it as add() does a control. Returns the built node as an Element, or nullptr for an empty ui::Spec, which adds nothing; a spec's .control field gives a typed observer.

orphan

std::unique_ptr<Element> orphan(const Element* child)

Takes child out of the grid alive and hands it back, for adding somewhere else. Focus inside it is cleared. Null when it is not one of this grid's children.

set_cell

void set_cell(const Element* child, std::size_t row, std::size_t column, std::size_t row_span = 1, std::size_t column_span = 1)

Moves a child to another cell and span. An element that is not a child of this grid is ignored.

set_row

void set_row(const Element* child, std::size_t row)

Moves child to row, keeping its column and spans.

set_column

void set_column(const Element* child, std::size_t column)

Moves child to column, keeping its row and spans.

background

const std::optional<Color>& background() const

The color filled behind the children; nullopt (the default) draws nothing. A ThemeColor is a theme resource, re-read whenever it is drawn (ui/BackgroundBrush.hpp).

background_resource

ThemeColor background_resource() const

The background as a theme resource, or nullptr when a fixed color (or nothing) is set.

set_background

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

Sets a fixed background color (nullopt for none); redraws.

set_background

void set_background(ThemeColor resource)

Sets the background to a theme resource; redraws.

background_transition

const std::optional<BrushTransition>& background_transition() const

With a transition set, a later change of what the background draws (a new color, or a resource under a new theme) blends from the color on screen (Animation.hpp's BrushTransition). nullopt (the default) changes it at once.

set_background_transition

void set_background_transition(std::optional<BrushTransition> transition)

Sets or clears the background transition.

border_brush

std::optional<Color> border_brush() const

The outline color; nullopt (the default) draws no border even with a non-zero border_thickness(). Nullopt as well while a ThemeColor is set.

border_brush_resource

ThemeColor border_brush_resource() const

The theme resource the border is drawn from, or null.

set_border_brush

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

A fixed outline color; replaces any resource. Redraws.

set_border_brush

void set_border_brush(ThemeColor resource)

An outline color read from the theme each time it is drawn; replaces any fixed color. Redraws.

border_thickness

const Thickness& border_thickness() const

The border width in DIPs per side (default zero); it insets the children like padding.

set_border_thickness

void set_border_thickness(const Thickness& thickness)

Sets the border width; updates the layout and redraws.

corner_radius

float corner_radius() const

The corner radius in DIPs, uniform for all four corners (default 0, square). It rounds the background and the border; the children are not clipped.

set_corner_radius

void set_corner_radius(float radius)

Sets the corner radius; redraws.

padding

const Thickness& padding() const

The room in DIPs between the border and the children (default zero).

set_padding

void set_padding(const Thickness& padding)

Sets the padding and updates the layout.

row_spacing

float row_spacing() const

The gap between adjacent rows, DIPs; 0 by default.

set_row_spacing

void set_row_spacing(float spacing)

Sets the row gap and updates the layout.

column_spacing

float column_spacing() const

The gap between adjacent columns, DIPs; 0 by default.

set_column_spacing

void set_column_spacing(float spacing)

Sets the column gap and updates the layout.

parallel_measure_safe

bool parallel_measure_safe() const override

True: measuring touches only the grid and its children.

for_each_child

void for_each_child(ChildVisitor visit) const override

Lists the children in the order they were added.

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 transition, then ticks every child.

struct GridLength

The size of one Grid row or column: Auto, a fraction of the remaining space, or fixed DIPs, optionally kept between a smallest and a largest size. Defaults to one fraction.

In a spec, the literals ui::AutoSize, 1_fr and 48_dip are shorthand for auto_(), fraction() and dip(), and ui::minmax(200_dip, 1_fr) adds the limits.

Example:

auto grid = std::make_unique<ui::GridControl>();
grid->add_column(ui::GridLength::dip(200.0f));     // a 200-DIP sidebar
grid->add_column(ui::GridLength::fraction());      // the rest
grid->add_row(ui::GridLength::auto_());            // a header sized to its content
grid->add_row(ui::GridLength::fraction(2.0f));     // twice the share of a fraction() row
grid->add_column(ui::minmax(ui::GridLength::dip(120.0f), ui::GridLength::fraction())); // never under 120

unit

GridUnit unit = GridUnit::Fraction

How value is read.

value

float value = 1.0f

The Fraction weight or the Dip size in DIPs; ignored for Auto.

min

float min = 0.0f

The smallest the track gets, DIPs; 0 by default.

max

float max = Unbounded

The largest the track gets, DIPs; ui::Unbounded (no limit) by default.

auto_

static GridLength auto_()

An Auto track, sized to its content.

fraction

static GridLength fraction(float weight = 1.0f)

A fraction of the remaining space, weighted by weight: a fraction(2) track gets twice what a fraction(1) track does.

dip

static GridLength dip(float dips)

A fixed track of dips DIPs.

Enumerations

GridUnit

enum class GridUnit

How a Grid track's GridLength::value is read.

Value
Auto sized to the largest single-span child in the track; value is ignored
Fraction a weighted share of the space left after Dip and Auto tracks; value is the weight
Dip a fixed size; value is DIPs

Functions

minmax

GridLength minmax(GridLength smallest, GridLength largest)

A Grid track kept between two sizes, as CSS's minmax(): ui::minmax(200_dip, 1_fr) is a fraction of the remaining space that never gets narrower than 200 DIPs, and ui::minmax(ui::AutoSize, 300_dip) sizes to its content up to 300 DIPs.

smallest is a Dip length, which sets the lower limit, or Auto, which sets none. largest decides how the track is sized. When both are Dip lengths, the track takes a fraction of the remaining space, kept between the two. The limits also hold while a Splitter resizes the track.

Example:

using namespace ui::literals;
ui::Grid{.columns = {ui::minmax(160_dip, 1_fr), 6_dip, 2_fr}, .content = {/* ... */}};

operator""_dip

GridLength operator""_dip(long double value)

A fixed track size in DIPs: 200_dip, 12.5_dip.

operator""_dip

GridLength operator""_dip(unsigned long long value)

Integer form of _dip.

operator""_fr

GridLength operator""_fr(long double weight)

A fraction of the remaining space: a weighted share of what is left after fixed and auto tracks, so a 2_fr track gets twice what a 1_fr one does. 0.5_fr.

operator""_fr

GridLength operator""_fr(unsigned long long weight)

Integer form of _fr.

operator""_dip

GridLength operator""_dip(long double value)

A fixed track size in DIPs: 200_dip, 12.5_dip.

operator""_dip

GridLength operator""_dip(unsigned long long value)

Integer form of _dip.

operator""_fr

GridLength operator""_fr(long double weight)

A fraction of the remaining space: a weighted share of what is left after fixed and auto tracks, so a 2_fr track gets twice what a 1_fr one does. 0.5_fr.

operator""_fr

GridLength operator""_fr(unsigned long long weight)

Integer form of _fr.

Constants

Name Type Value
AutoSize GridLength GridLength::auto_() A track sized to its content.

Tests

  • tests/test_grid.cpp