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|=¶
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¶
An empty Spec: no content. Building it yields a null node, which every container skips.
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¶
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¶
Move-only: a Spec owns the node it will hand over.
operator=¶
Spec¶
operator=¶
build¶
Satisfies SpecLike, so a Spec composes anywhere a spec does. Leaves it empty.
take¶
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¶
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¶
The built elements, in order. Public so a parent's build() can drain it.
SpecList¶
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¶
A list of one: a single spec, without braces. An empty ui::Spec gives an empty list.
SpecList¶
A list of one already-built subtree; a null pointer gives an empty list.
SpecList¶
operator=¶
SpecList¶
operator=¶
empty¶
True when the list holds no elements.
size¶
The number of elements held.
push_back¶
Build spec and append it, as one more entry of a braced list would be. An empty Spec is skipped.
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¶
The control class S builds.
Control¶
An empty handle; a spec's build() fills it.
Control¶
Another handle to the same control.
operator=¶
Points at the same control as other.
get¶
The control, or nullptr before the spec was built or after the control was destroyed.
operator->¶
Member access on the control. The handle must not be empty.
operator*¶
The control itself. The handle must not be empty.
operator bool¶
True while the built control is alive.
operator Type *¶
The control's pointer, for an API that takes one.
operator=¶
Forgets the control.
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¶
No handle: build() publishes nothing.
ControlOut¶
Publish into handle.
ControlOut¶
Publish into *handle, or nowhere when it is null: for a handle that is only wanted sometimes (.control = first ? &handle : nullptr).
publish¶
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¶
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¶
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¶
Move-only: it owns the layout it will hand over.
operator=¶
LayoutSpec¶
operator=¶
take¶
Moves the layout out; null if none was given.
operator bool¶
True while a layout is held.
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¶
No link.
ControlLink¶
A link to the control handle holds or will hold.
operator bool¶
True when a handle was given.
resolve¶
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.
class ElementLink¶
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¶
No link.
ElementLink¶
A link to the control handle holds or will hold.
operator bool¶
True when a handle was given.
resolve¶
For a spec's build(): see ControlLink::resolve(). Consumes the link.
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¶
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¶
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|¶
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¶
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¶
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¶
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¶
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