Skip to content

Spec Vocabulary

Headers: winui/ui/spec/Common.hpp, winui/ui/spec/Spec.hpp, winui/ui/spec/Control.hpp, winui/ui/spec/LayoutSpec.hpp, winui/ui/spec/ItemsModel.hpp, winui/ui/spec/Dynamic.hpp. Spec: winui/ui/spec/Common.hpp.

ui::Style

A named, composable bundle of layout defaults: the margin, the six sizes and the two alignments every spec carries. .style = app_styles::Card gives every card the same room, and works on every spec.

Composition is compile-time: styles::Spaced | styles::Centered is a constant expression, with the right-hand side winning wherever it sets a field. Unset fields (std::nullopt) pass through, so two partial styles combine. A field set on the spec itself wins over the style's.

Typography is not part of a style: a ui::Text takes .role (a step of the theme's type ramp) or .text_style, and a control with a foreground takes .foreground.

Example:

namespace app_styles
{
inline constexpr ui::Style Card = ui::styles::Spaced | ui::Style{.min_width = 200.0f};
}

window.set_content(ui::VerticalStack{.content = {
    ui::Text{.text = "Saved 2 minutes ago", .role = ui::TextRole::Caption, .style = app_styles::Card},
    ui::Button{.text = "Open", .style = app_styles::Card},
    // An explicit field beats the style's: 16 DIP margin, the minimum width kept.
    ui::Button{.text = "Close", .style = app_styles::Card, .margin = ui::Thickness{16.0f}},
}});

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

ui::Style{
    .margin, .width, .height, .min_width, .min_height, .max_width, .max_height,
    .horizontal_alignment, .vertical_alignment
}

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

operator|=

Style& operator|=(const Style& other)

Merges other into this style: other wins wherever it sets a field; unset fields pass through. Returns *this.

class Spec

Any piece of UI: the one type-erased, move-only description value.

A Spec holds the already-built node of whatever spec (or hand-built unique_ptr<Element>) it was made from. It is what a component function returns, what a slot parameter takes, the type of every single-content field (.content, .pane, .anchor, ...) and of each entry in a .content list, so the same value composes in all four places. It is consumed once. A default-constructed Spec is empty: a component that decided to show nothing, or a content field left unset. Containers skip an empty one.

Converts implicitly from any SpecLike value and from a std::unique_ptr to an Element subclass, which is what lets a braced list mix specs and hand-built controls.

A braced list of slots (Grid cells, Canvas items, tabs) is a std::initializer_list, which only hands out const references. take() is const so the node can still be moved out of each slot. Each list drains its entries inside the full-expression that created it, so nothing reads a Spec after its node was moved out.

Handlers captured in a spec returned from a function must capture by value, or capture a reference to state that outlives the window; taking Property<T>& parameters makes that the easy thing.

Example:

ui::Spec user_row(ui::Property<std::string>& name, std::string email)
{
    return ui::HorizontalStack{
        .spacing  = 8,
        .content  = {
            ui::Icon{ .source = ui::glyphs::Contact, .accessibility = { .decorative = true } },
            ui::VerticalStack{ .content = {
                ui::Text{ .text = name },
                ui::Text{ .text = std::move(email), .role = ui::TextRole::Caption },
            }},
        },
    };
}

ui::Property<std::string> alice{"Alice"}, bob{"Bob"};
auto note = std::make_unique<ui::TextControl>("Hand-built"); // a control made without a spec

window.set_content(ui::VerticalStack{.content = {
    user_row(alice, "[email protected]"), // a component's result
    user_row(bob, "[email protected]"),
    ui::Text{.text = "Sales"},            // a spec: built as the list is formed
    std::move(note),                      // an existing subtree: adopted unchanged
}});

bool show_footer = true;
ui::Spec footer = show_footer ? ui::Spec{ui::Text{.text = "Footer"}} : ui::Spec{};

Spec

Spec() = default

An empty Spec: no content. Building it yields a null node, which every container skips.

Spec

template <SpecLike S>
Spec(S&& spec)
    requires (!std::same_as<std::remove_cvref_t<S>, Spec>)

A spec, built the moment it becomes a Spec. Spec itself is excluded, so this never takes the place of Spec's move constructor.

Spec

template <typename T>
Spec(std::unique_ptr<T> node)
    requires std::derived_from<T, Element>

An already-built subtree, placed into a declared tree unchanged. This is how a control without a spec, or a subtree assembled in a loop, combines with declared UI. Takes ownership; no copy of the node is made.

Spec

Spec(const Spec&) = delete

Move-only: a Spec owns the node it will hand over.

operator=

Spec& operator=(const Spec&) = delete

Spec

Spec(Spec&&) = default

operator=

Spec& operator=(Spec&&) = default

build

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

Satisfies SpecLike, so a Spec composes anywhere a spec does. Leaves it empty.

take

std::unique_ptr<Element> take() const

Move the built node out. Const because initializer_list only ever hands out const references; see the class comment for why that is sound. Calling it twice yields a null second time, which SpecList never does.

operator bool

operator bool() const

True while a node is held: not default-constructed and not yet built or take()n.

class SpecList

A built, owned list of elements. Move-only: it owns the built elements from the moment it is constructed.

The type of a container's .content field. Written as a braced list in the common case, or as a single spec without braces when there is only one; build one up in a loop with push_back(), then move it into the field.

Example:

std::vector<std::string> names{"Ada", "Grace", "Linus"};
ui::SpecList rows;
for (const auto& name : names)
{
    rows.push_back(ui::CheckBox{.text = name});
}

window.set_content(ui::VerticalStack{.spacing = 4, .content = std::move(rows)});

// One element needs no braces.
ui::VerticalStack{.spacing = 4, .content = ui::Text{.text = "Just this"}};

nodes

std::vector<std::unique_ptr<Element>> nodes

The built elements, in order. Public so a parent's build() can drain it.

SpecList

SpecList() = default

An empty list.

SpecList

template <typename... S>
SpecList(S&& ... items)
    requires (sizeof...(S) > 1 && (std::constructible_from<Spec, S &&> && ...))

A braced list of two or more: builds each entry (a spec or a unique_ptr) into nodes, in order. An empty Spec is skipped: it stands for no element.

SpecList

template <SpecLike S>
SpecList(S&& spec)

A list of one: a single spec, without braces. An empty ui::Spec gives an empty list.

SpecList

template <typename T>
SpecList(std::unique_ptr<T> node)
    requires std::derived_from<T, Element>

A list of one already-built subtree; a null pointer gives an empty list.

SpecList

SpecList(const SpecList&) = delete

operator=

SpecList& operator=(const SpecList&) = delete

SpecList

SpecList(SpecList&&) = default

operator=

SpecList& operator=(SpecList&&) = default

empty

bool empty() const

True when the list holds no elements.

size

std::size_t size() const

The number of elements held.

push_back

void push_back(Spec spec)

Build spec and append it, as one more entry of a braced list would be. An empty Spec is skipped.

class Control

template <typename S> class Control

A typed, non-owning handle to the control a spec built.

S is the spec type; the control's type comes from the spec's using control_type = ...; line, so ui::Control<ui::Button> points at a ui::ButtonControl. Declare one, hand it to the spec's .control field, and after the tree is built it reaches the live control. It changes no ownership: the tree owns the control, and the handle is empty until build() has run, empty again once the control is destroyed (it watches the control). That includes a control built inside a subtree that was then thrown away, such as the children of a spec passed to a false ui::when(). Most controls need none; name only what you address later.

It is typed, so handing a ui::Control<ui::TextBox> to a ui::Button is a compile error on that line. Where an API wants the control's pointer (Element*, ButtonControl*), the handle converts to it.

Example:

ui::Control<ui::Button> save;
ui::Control<ui::TextBox> name;

window.set_content(ui::VerticalStack{.content = {
    ui::TextBox{.placeholder_text = "Name", .control = name},
    ui::Button{.text = "Save", .on_click = [&] { save->set_text("Saved " + name->text()); }, .control = save},
}});

save->set_enabled(false); // the tree is built: the handles are live

Type

using ui::Control< S >::Type = typename S::control_type

The control class S builds.

Control

Control() = default

An empty handle; a spec's build() fills it.

Control

Control(const Control& other)

Another handle to the same control.

operator=

Control& operator=(const Control& other)

Points at the same control as other.

get

Type* get() const

The control, or nullptr before the spec was built or after the control was destroyed.

operator->

Type* operator->() const

Member access on the control. The handle must not be empty.

operator*

Type& operator*() const

The control itself. The handle must not be empty.

operator bool

operator bool() const

True while the built control is alive.

operator Type *

operator Type *() const

The control's pointer, for an API that takes one.

operator=

Control& operator=(std::nullptr_t)

Forgets the control.

class ControlOut

template <typename S> class ControlOut

The type of every spec's .control field: where build() publishes the control it made.

It converts from a ui::Control<S> handle, so a spec is written .control = save, and it only remembers where that handle is; the handle itself must outlive the build. App code never names this type: a component function that passes a handle on to the spec it returns takes a ui::Control<S>&, or a ui::Control<S>* defaulted to nullptr when the handle is optional, and both convert. A custom spec declares its .control field as a ControlOut of itself.

Example:

ui::Spec save_button(ui::Property<bool>& dirty, ui::Control<ui::Button>* control = nullptr)
{
    return ui::Button{.text = "Save", .enabled = dirty, .control = control};
}

ui::Property<bool>      dirty{false};
ui::Control<ui::Button> save;
window.set_content(save_button(dirty, &save)); // `save` reaches the built button

ControlOut

ControlOut() = default

No handle: build() publishes nothing.

ControlOut

ControlOut(Control<S>& handle)

Publish into handle.

ControlOut

ControlOut(Control<S>* handle)

Publish into *handle, or nowhere when it is null: for a handle that is only wanted sometimes (.control = first ? &handle : nullptr).

publish

template <typename C>
void publish(C* node) const

Stores node in the handle, if there is one. Called last in a spec's build(), through ui::publish_control().

class LayoutSpec

How generated children are arranged: the type of every .layout field.

A layout is written as a plain value, .layout = ui::TileLayout{.min_row_spacing = 8}, with no make_unique and no setters. A name ending in Layout is such a value and never a control: there is no ui::Control<ui::TileLayout>. An empty LayoutSpec leaves the field's default in place, which for ui::ForEach is a vertical stack.

Which to reach for: hand-written children pick a layout control (ui::VerticalStack, ui::Grid, ui::WrapPanel); children generated from a collection pick a .layout.

A layout of your own is a class: derive from ui::VirtualizingLayout or ui::NonVirtualizingLayout and hand a std::make_unique<MyLayout>() to the same field.

Example:

struct Photo
{
    std::string title;
};
ui::ObservableVector<Photo> photos;

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

LayoutSpec

LayoutSpec() = default

No layout: the field's own default applies.

LayoutSpec

template <LayoutLike L>
LayoutSpec(L&& layout)
    requires (!std::same_as<std::remove_cvref_t<L>, LayoutSpec>)

A layout value: built the moment it becomes a LayoutSpec.

LayoutSpec

template <typename T>
LayoutSpec(std::unique_ptr<T> layout)
    requires std::derived_from<T, Layout>

A layout object the app made itself, for a layout class of its own or one it keeps a pointer to and changes later. Takes ownership.

LayoutSpec

LayoutSpec(const LayoutSpec&) = delete

Move-only: it owns the layout it will hand over.

operator=

LayoutSpec& operator=(const LayoutSpec&) = delete

LayoutSpec

LayoutSpec(LayoutSpec&&) = default

operator=

LayoutSpec& operator=(LayoutSpec&&) = default

take

std::unique_ptr<Layout> take()

Moves the layout out; null if none was given.

operator bool

operator bool() const

True while a layout is held.

template <typename S> class ControlLink

The type of a spec field that names another control by its handle: InkToolbar's .target, TeachingTip's .target, RichText's .overflow. Written .target = canvas with a ui::Control<ui::InkCanvas>, it links the two controls in either build order: at once when the target is already built, or as soon as the target's spec fills the handle. When the target is destroyed the control lets go of it, and a later build that fills the handle again links the new one. The handle must outlive the build, as for .control.

Example:

ui::Control<ui::InkCanvas> canvas;
window.set_content(ui::VerticalStack{.content = {
    ui::InkToolbar{.target = canvas}, // built first, linked once the canvas is built
    ui::InkCanvas{.height = 300, .control = canvas},
}});

ControlLink() = default

No link.

ControlLink(Control<S>& handle)

A link to the control handle holds or will hold.

operator bool

operator bool() const

True when a handle was given.

resolve

template <typename Fn>
void resolve(Element& owner, Fn link) const

For a spec's build(): calls link(target) now if the handle already holds a control, and again each time a build fills the handle, as long as owner lives. owner is the control being built, which keeps the target as an ElementWatch (or unlinks itself in its destructor) so a destroyed target never leaves it holding a dangling pointer.

ControlLink for a field that takes any control: TeachingTip's .target points at a button, a text box or anything else that has a handle. Written .target = save with any ui::Control<S>; it links in either build order and lets go of a destroyed target, as ControlLink does.

ElementLink() = default

No link.

template <typename S>
ElementLink(Control<S>& handle)

A link to the control handle holds or will hold.

operator bool

operator bool() const

True when a handle was given.

resolve

void resolve(Element& owner, Callback<void(Element*)> link)

For a spec's build(): see ControlLink::resolve(). Consumes the link.

concept LayoutLike

template <typename L> concept LayoutLike

Anything with an rvalue-qualified build() producing an owned Layout: what a .layout field takes. The four built-in layout values satisfy it, and so does an app's own.

concept SpecLike

template <typename S> concept SpecLike

Anything with an rvalue-qualified build() producing an owned Element. Because it is a concept, a mistake is reported at the caller's own line. Passing a non-spec into a child list gives "no conversion from 'int' to 'Spec'", not a template backtrace.

An app's own spec type satisfies it by providing std::unique_ptr<ui::Element> build() &&, and then composes anywhere a built-in spec does.

Example:

struct LabeledField // a caller-defined spec
{
    std::string label;
    ui::Property<std::string>* value = nullptr;

    [[nodiscard]] std::unique_ptr<ui::Element> build() &&
    {
        return ui::build(ui::VerticalStack{.spacing = 4, .content = {
            ui::Text{.text = label},
            ui::TextBox{.text = ui::bind(*value)},
        }});
    }
};
static_assert(ui::SpecLike<LabeledField>);

ui::Property<std::string> name;
window.set_content(ui::VerticalStack{.content = {LabeledField{"Name", &name}}});

Aliases

ItemTemplate

using ui::ItemTemplate = Callback<Spec(std::size_t index)>

An item template in a spec: the Spec for item index, which may pick a different look by the item. Called only for the items a control creates rows for, and it may be called again for an item later, so it indexes into a model the caller keeps alive for the life of the control.

SelectedItem

using ui::SelectedItem = std::conditional_t<std::copy_constructible<T>, std::optional<T>, std::optional<std::size_t>>

What a typed view's on_selection_changed receives: a copy of the selected item (the first one when several are), or unset when nothing is selected. A copy is safe to keep. For an item type that cannot be copied it is the selected index instead.

Functions

operator|

Style operator|(Style lhs, const Style& rhs)

Composes two styles into a new one; rhs wins wherever it sets a field. Usable in constant expressions: constexpr Style s = styles::Spaced | styles::Leading;.

apply

template <typename T, typename Fn>
void apply(Binding<T>& binding, Element& target, Fn sink)
    requires std::invocable<Fn&, const T&>

Seed a control property from a Binding and, if the binding is reactive, keep it in sync for as long as the node lives.

Together with write_back(), shared_writer(), write_back_value(), apply_common(), adopt(), ui::build() and SpecList, it is what the build() of a spec for your own control calls. These helpers keep the same compatibility promise as Element.

sink is called once immediately with the binding's initial value, then again on every change, through a Subscription the target Element owns. That subscription dies with the node (see core/Property.hpp).

Example:

// A piece of a spec's build() && (see apply_common() for the whole shape):
std::unique_ptr<ui::TextControl> build_label(ui::Binding<std::string> text)
{
    auto node = std::make_unique<ui::TextControl>();
    ui::apply(text, *node, [raw = node.get()](const std::string& v) { raw->set_text(v); });
    return node;
}

write_back

template <typename T, typename First, typename... Rest>
Callback<void(First, Rest...)> write_back(Binding<T>& binding, Callback<void(First, Rest...)> handler)

The change handler of an editable field: writes the control's new value back through a two-way binding, then runs the spec's own handler. Returns handler unchanged when the binding is one-way, and an empty callback when there is nothing to do.

The first argument of the handler is the new value. Call it in build() where the control's on_*_changed is installed, after apply() has seeded the value:

void bind_value(ui::SliderControl& node, ui::Binding<double>& value, ui::Callback<void(double)> on_value_changed)
{
    ui::apply(value, node, [raw = &node](double v) { raw->set_value(v); });
    if (auto changed = ui::write_back(value, std::move(on_value_changed)))
    {
        node.on_value_changed(std::move(changed));
    }
}

shared_writer

template <typename T>
std::shared_ptr<typename Binding<T>::Write> shared_writer(Binding<T>& binding)

The writer of a two-way binding, shared between the handlers of an open/closed field, or null when the binding is one-way. See write_back_value().

write_back

template <typename W, typename First, typename... Rest>
Callback<void(First, Rest...)> write_back(const std::shared_ptr<W>& writer, Callback<void(First, Rest...)> handler)

As write_back(), with a writer already taken by shared_writer(), for a field whose apply() sink also writes back what the control coerced the bound value to (a Slider's clamp), so the property ends up equal to the control without running the spec's handler:

void bind_value(ui::SliderControl& node, ui::Binding<double>& value, ui::Callback<void(double)> on_value_changed)
{
    const auto write = ui::shared_writer(value);
    ui::apply(value, node, [raw = &node, write](const double& v) {
        raw->set_value(v);
        if (write && raw->value() != v) { (*write)(raw->value()); }
    });
    if (auto changed = ui::write_back(write, std::move(on_value_changed)))
    {
        node.on_value_changed(std::move(changed));
    }
}

write_back_value

template <typename T, typename... Args>
Callback<void(Args...)> write_back_value(const std::shared_ptr<typename Binding<T>::Write>& writer, std::type_identity_t<T> value, Callback<void(Args...)> handler)

As write_back(), for a field whose handler carries no value: an open/closed state, written as value when the handler runs (.is_open with on_opened and on_closed). Returns handler unchanged when writer is null.

void bind_open(ui::PopupControl& node, ui::Binding<bool>& is_open, ui::Callback<void()> on_opened,
               ui::Callback<void()> on_closed)
{
    const auto open = ui::shared_writer(is_open);
    if (auto opened = ui::write_back_value<bool>(open, true, std::move(on_opened)))
    {
        node.on_opened(std::move(opened));
    }
    if (auto closed = ui::write_back_value<bool>(open, false, std::move(on_closed)))
    {
        node.on_closed(std::move(closed));
    }
}

apply_common

template <typename S>
void apply_common(S& spec, Element& target)

Apply the presentation and layout fields every spec carries.

Duck-typed on field names, so a spec of your own control works if it declares the common fields flat, with no shared base class.

The nine layout fields (margin, the six sizes, the two alignments) are bindable and optional. Precedence: a bound field always wins, a constant that is set wins, and .style fills in a field left unset. So .style = styles::Spaced, .margin = Thickness{16} gives 16, .style = styles::Spaced alone gives 8, and .width = size follows size whatever the style says. A set value means what it says: .width = 0 is zero wide, and only an unset width is Auto. A bound std::nullopt puts the control back to its own default (Auto, no limit, no margin, Stretch). Moves spec.accessibility out when it is not empty.

.visible maps to Element::set_visibility(): false collapses the control, which then takes no room in layout, draws nothing, gets no input and leaves the tab ring. Nothing is destroyed, so a collapsed subtree keeps its state (typed text, scroll offsets, .control handles) and its bindings stay live. A spec without the field is always visible.

A caller-defined spec gets the whole common-field vocabulary by declaring the same fields and calling this from its build().

Example:

struct Badge // a caller-defined spec
{
    ui::Binding<std::string> text{};

    ui::Style                                  style{};
    ui::Binding<std::optional<ui::Thickness>> margin{};
    ui::Binding<std::optional<float>>         width{}, height{}, min_width{}, min_height{};
    ui::Binding<std::optional<float>>         max_width{}, max_height{};
    ui::Binding<std::optional<ui::Alignment>> horizontal_alignment{}, vertical_alignment{};
    ui::Binding<bool>                         visible{true};
    ui::Accessibility                         accessibility{};

    using control_type = ui::TextControl;
    ui::ControlOut<Badge> control{};

    [[nodiscard]] std::unique_ptr<ui::Element> build() &&
    {
        auto node = std::make_unique<ui::TextControl>();
        ui::apply(text, *node, [raw = node.get()](const std::string& v) { raw->set_text(v); });
        ui::apply_common(*this, *node);
        ui::publish_control(control, node.get());
        return node;
    }
};

window.set_content(ui::VerticalStack{.content = {Badge{.text = "New", .style = ui::styles::Spaced}}});

adopt

Element* adopt(Element& parent, std::unique_ptr<Element>& node)

Adopt an owned child node into parent, wiring the parent link the same way every container's own add() does. Only sets the parent pointer: node keeps ownership, so the caller must store it in a member of parent that outlives the link. Returns the node as a non-owning observer, or nullptr when node is empty.

publish_control

template <typename S, typename C>
void publish_control(const ControlOut<S>& out, C* node)

Publish the built node through a spec's .control field, if a handle was given. Call it last in build(), after handlers are installed; see apply_common() for an example.

content

template <typename... S>
SpecList content(S&& ... specs)
    requires (std::constructible_from<Spec, S &&> && ...)

The variadic form of the brace list: builds each argument (a spec or a unique_ptr) into a SpecList, skipping empty ones. Useful where a list is passed through a function and braces would not deduce. To build a list in a loop, use SpecList::push_back().

Example:

ui::SpecList actions = ui::content(ui::Button{.text = "OK"}, ui::Button{.text = "Cancel"});

window.set_content(ui::HorizontalStack{.spacing = 8, .content = std::move(actions)});

when

template <typename S>
Spec when(bool condition, S&& spec)
    requires std::constructible_from<Spec, S&&>

A spec that is only there when condition holds: spec when it is true, an empty ui::Spec (no element at all) when it is false.

A false condition skips the spec's own build(), but C++ evaluates the argument first: the children of a container spec (its .content list) are already built by then, and are thrown away again. Their .control handles end up empty. Pass a callable returning the spec instead (the other overload) to build nothing at all when the condition is false.

The condition is read once, while the tree is built. For one that follows state, use the .visible field every spec has, ui::Lazy, or ui::Match.

Example:

bool is_admin = true;
window.set_content(ui::VerticalStack{.content = {
    ui::Text{.text = "Report"},
    ui::when(is_admin, ui::Button{.text = "Delete all"}),
}});

when

template <typename F>
Spec when(bool condition, F&& make)
    requires std::invocable<F&&> && std::constructible_from<Spec, std::invoke_result_t<F&&>>

The lazy form of ui::when(): make is called only when condition holds, so a false condition builds nothing, children included. make returns a spec (or anything a ui::Spec is made from).

Example:

bool                    is_admin = true;
ui::Control<ui::Button> delete_all;
window.set_content(ui::VerticalStack{.content = {
    ui::Text{.text = "Report"},
    ui::when(is_admin, [&] {
        return ui::HorizontalStack{.spacing = 8, .content = {
            ui::Button{.text = "Delete all", .control = delete_all},
            ui::Button{.text = "Export"},
        }};
    }),
}});

build

template <SpecLike S>
std::unique_ptr<Element> build(S&& spec)

Builds a spec into an owned node, for handing a declared subtree to an imperative API that takes a unique_ptr.

Constants

Name Type Value
Compact Style {.margin = Thickness{2.0f}} 2 DIP margin all round.
Spaced Style {.margin = Thickness{8.0f}} 8 DIP margin all round.
Centered Style {.horizontal_alignment = Alignment::Center, .vertical_alignment = Alignment::Center} Centered on both axes in the slot.
Leading Style {.horizontal_alignment = Alignment::Leading} Left-aligned in the slot.
Trailing Style {.horizontal_alignment = Alignment::Trailing} Right-aligned in the slot.

Tests

  • tests/test_spec.cpp