Conventions¶
The rules the language or the ownership model enforces. They hold for every spec and every control, so each reference page can leave them out.
Spec Field Order¶
Every spec declares its fields in the same order of blocks:
- Properties: what the control shows and holds (
.text,.value,.severity), themselves in a fixed order: the texts that label the control (.header,.title, a button's.text,.description,.placeholder_text), then its main value (.value,.checked,.items,.text,.selected_index,.is_open), then what limits it (.min,.max,.step,.max_length), then the other properties, and the appearance properties last (.role,.variant, colors, fonts,.padding). - Appearance and layout:
.styleand the common fields below, then.tool_tip, then.enabledon a control that takes input, then.accessibility. - Behavior:
.command, then the.on_*handlers, the control's main event first and the generic pointer, focus and key events last. .control: theui::Control<...>handle that receives the built control.- Children:
.contentand the other child fields, always last.
C++ requires designated initializers in declaration order, so a call site has to follow this order too. Write .text before .on_click and .spacing before .content, or the code does not compile:
ui::Button{.text = "Save", .on_click = [&] { save(); }} // compiles
ui::Button{.on_click = [&] { save(); }, .text = "Save"} // error: out of declaration order
Leave out any field you do not need. Each spec's reference page opens with every field in declaration order, common ones included, so that list is also the order to write them in.
New fields are appended or inserted into their block; existing fields are never reordered. Code that compiles against one version keeps compiling when fields are added.
Common Fields on Every Spec¶
Every spec has the same fields in its appearance and layout block, declared flat in each spec rather than inherited. An aggregate with a base class would have to be initialized positionally (Button{{.margin = 4}, ...}), which is exactly what designated initializers are there to avoid.
| Field | Type | Default | Meaning |
|---|---|---|---|
style |
ui::Style |
{} |
A named style. Any field it sets fills in a layout field below that was left unset. |
margin |
ui::Binding<std::optional<ui::Thickness>> |
unset | Outer margin, in DIPs. Unset is no margin. |
width |
ui::Binding<std::optional<float>> |
unset | Fixed width, in DIPs. Unset is Auto. |
height |
ui::Binding<std::optional<float>> |
unset | Fixed height, in DIPs. Unset is Auto. |
min_width |
ui::Binding<std::optional<float>> |
unset | Minimum width, in DIPs. Unset is none. |
min_height |
ui::Binding<std::optional<float>> |
unset | Minimum height, in DIPs. Unset is none. |
max_width |
ui::Binding<std::optional<float>> |
unset | Maximum width, in DIPs. Unset is no limit. |
max_height |
ui::Binding<std::optional<float>> |
unset | Maximum height, in DIPs. Unset is no limit. |
horizontal_alignment |
ui::Binding<std::optional<ui::Alignment>> |
unset | Horizontal alignment within the slot the parent gives it. Unset is Stretch. |
vertical_alignment |
ui::Binding<std::optional<ui::Alignment>> |
unset | Vertical alignment within that slot. Unset is Stretch. |
visible |
ui::Binding<bool> |
true |
false collapses the control: no room in layout, not drawn, no input, no tab stop. Nothing is destroyed, so it keeps its state. |
tool_tip |
ui::Binding<std::string> |
"" |
Shown when the pointer rests on the control. Empty leaves the control's own tooltip, such as a command's description. |
enabled |
ui::Binding<bool> |
true |
Only on controls that take input, always right after tool_tip. false grays the control out and takes no input. |
accessibility |
ui::Accessibility |
{} |
Name, description and test id for UI Automation. |
A set value means what it says: .width = 0.0f is zero wide, and only an unset width is Auto. A bound std::nullopt puts the control back to its own default.
A layout field that is set on the spec wins over .style, and .style fills in the ones left unset. So .style = ui::styles::Spaced alone gives an 8 DIP margin, and .style = ui::styles::Spaced, .margin = ui::Thickness{16.0f} gives 16. A bound field always wins. Styles compose with | at compile time, the right-hand side winning where both set a field:
inline constexpr ui::Style note = ui::styles::Compact | ui::styles::Leading;
ui::Text{.text = "Saved 2 minutes ago", .style = note}
A style holds only the margin, the six sizes and the two alignments. Typography goes through a ui::Text's .role or .text_style.
A spec you write yourself gets the same vocabulary by declaring the same fields and calling ui::apply_common(*this, *node) in its build().
Values and Bindings¶
A property field is usually a ui::Binding<T>. It takes a plain value, or a ui::Property<T> written as it is, and the source says which:
ui::Property<std::string> title{"Untitled"};
ui::Text{.text = "Static label"} // a value: set once
ui::Text{.text = title} // a property: follows every change to title
ui::Text{.text = ui::bind(title, [](const std::string& t) { return t + " - Editor"; })}
ui::bind() is only needed to convert: a function from the property's value to the field's, or several properties into one value. title.get() is the explicit snapshot, a value read once.
On a field the user edits (a text box's .text, a slider's .value, a toggle's .checked, a list's .selected_index, an .is_open), a property is two-way: the control shows it, and what the user enters is written back into it before the field's own on_*_changed handler runs. On a display field it is one-way, as there is nothing to write.
ui::Property<float> volume{50.0f};
ui::Slider{.value = volume} // dragging changes volume
ui::Slider{.value = ui::bind_one_way(volume)} // the slider only shows it
A binding with a conversion (ui::bind(prop, fn)), a const property and a ui::Computed are always one-way, and ui::bind_one_way(prop) opts an editable field out, for an input whose edits the app checks before storing. Property::set() does nothing when the value is unchanged, so the round trip stops after one round.
Collections and Layouts¶
There are two kinds of collection spec. A repeater turns a collection into children and nothing else: ui::ForEach. A view is a finished control around one, with scrolling, selection and keyboard navigation: ui::ListView, ui::TileView, ui::TreeView, ui::TableView. If you want a control, use a view; if you want to generate children, use ui::ForEach.
ui::Property<std::vector<Photo>> photos{load_photos()};
ui::ForEach<Photo>{
.items = photos, // the collection
.layout = ui::TileLayout{.min_item_width = 160.0f}, // how they are arranged
.item = [](const Photo& p) -> ui::Spec { return ui::Text{.text = p.title}; }, // how one looks
}
A collection spec names its item type, ui::ForEach<Photo>, ui::ListView<Contact>: C++ works out a spec's type only from the field's own type, which a Property is not. ui::ForEach builds every item and keeps each row's state by default. .virtualize = true is the opt-in for a long collection inside a ui::Scroll: only the rows in and near view exist, so state then belongs in the item.
.items takes three sources, which differ in how a change reaches the rows:
| Source | A change | .item receives |
|---|---|---|
a ui::ObservableVector<T> |
One row built per insert, one destroyed per removal, a moved item keeps its row. | T&, valid while the item is in the collection |
a ui::Property<std::vector<T>> with .key |
Rows follow their keys through any reorder; a row is rebuilt when its item compares unequal. | const T& to a copy, not to be kept |
the same without .key |
The rows before and after the edit are kept, those in between rebuilt. | const T& to a copy, not to be kept |
a plain std::vector<T> |
None, it is copied once. | const T& to a copy, not to be kept |
The views take .items from the same three sources, and their handlers receive the item:
| View | .key |
Handlers that receive the item |
|---|---|---|
ui::ListView, ui::TileView |
yes | on_item_click(const T&), on_item_invoked(const T&), on_selection_changed(std::optional<T>), a copy, unset when nothing is selected |
ui::TableView |
yes | on_selection_changed(std::optional<T>) |
ui::TreeView |
yes, a string or number | on_item_click(const T&), on_item_invoked(const T&), on_expanding(const T&), on_selection_changed(std::optional<T>) |
A const T& is into the view's copy of the items, or into the ui::ObservableVector, so do not keep it past the handler; the std::optional<T> a selection handler gets is a copy, safe to keep. ui::TreeView<T> takes its top-level items in .items and finds their children through .children; ui::TreeNode is the ready-made item. In a ui::ListView or ui::TileView, a drag reorder is written back into the bound collection. A list of strings needs no .item: ui::ListView<std::string>{.items = {"Inbox", "Sent"}}. The controls stay index-based: through .control, an item is its position.
Structure That Follows State¶
Nothing in libwinui compares two trees. Structure that depends on state is written with one of these, and each says in its name whether it keeps what the user did inside the subtree:
| Situation | Tool | Keeps state |
|---|---|---|
| Decided once, while the tree is built | ui::when(condition, spec), or an empty ui::Spec{} |
n/a |
| Show or hide, cheap subtree | .visible = shown (a ui::Property<bool>) |
yes |
| Show or hide, expensive subtree | ui::Lazy{.visible, .content} |
yes (.unload = true: no) |
| One of several, state should reset | ui::Match{.value, .content} |
no |
| One of several, state should persist | ui::Match{.keep = true} |
yes |
| A list you edit in place | ui::ForEach over a ui::ObservableVector |
yes, per item |
| A list you assign as a whole | ui::ForEach{.items, .key} |
yes, per key |
| A long list | ui::ForEach{.virtualize = true} or a view |
per realized row |
| State that belongs to one component | ui::State<S>{.initial, .content} |
as long as the component |
A condition over several properties is ui::bind(fn, a, b, ...): the function first, then the properties it reads, computed again when any of them changes.
A layout is a plain value handed to a .layout field, with no make_unique and no setters. A name ending in Layout is never a control.
| Arrangement | Layout control (hand-written children) | Layout value (generated children) | View |
|---|---|---|---|
| a line | ui::VerticalStack, ui::HorizontalStack |
ui::StackLayout |
ui::ListView |
| a wrapping line | ui::WrapPanel |
ui::WrapLayout |
|
| wrapping cells | ui::TilePanel (children may span) |
ui::TileLayout |
ui::TileView |
| justified lines | ui::LinedFlowLayout |
||
| a table | ui::TableView |
||
| defined rows and columns | ui::Grid |
Hand-written children pick a layout control; children generated from a collection pick a .layout. ui::ForEach and ui::TileView take one: ui::TileView{.layout = ui::WrapLayout{}} keeps the view's selection, keyboard navigation and drag reordering and only changes where the cells go. ui::Grid, ui::Canvas and ui::RelativePanel exist only as controls, because they need placement data per child. ui::Canvas is the one for exact positions: each child carries its .left and .top in DIPs and nothing is worked out for it, see Layout. A layout of your own is a class derived from ui::VirtualizingLayout or ui::NonVirtualizingLayout, handed to the same field as a std::unique_ptr.
Ownership¶
Every node in the tree has exactly one owner, its parent, which holds it in a std::unique_ptr. There is no shared ownership anywhere in the tree, and destroying a node destroys its whole subtree, in order.
Window::set_content()and a container'sadd()take ownership and return a plain pointer to the node. That pointer is an observer: use it, never delete it..control = my_button, withui::Control<ui::Button> my_button;, gives you the same kind of observer from a spec, typed by the spec's name. It is filled when the spec is built, so it is set by the time the statement that built the tree has finished.my_button->reaches theui::ButtonControl.- An observer is valid as long as the node is in the tree. A
ui::Matchwithout.keeprebuilds its subtree when the value it watches changes, and observers published from inside the old subtree dangle after that. The same goes for aui::ForEachrow whose item was removed and for aui::Lazywith.unload. - A spec is consumed by its
build(), which is&&-qualified. To build the same description twice, write a function that returns a fresh spec each time.
A hand-built std::unique_ptr<Element> can go anywhere a spec can, in a child list or a content field, and is adopted unchanged.
Handler Capture¶
Handlers are ui::Callback<Sig>, a std::move_only_function, so a handler can capture move-only state such as a std::unique_ptr. ui::Action is Callback<void()>.
What a handler captures by reference has to outlive the control. In main(), [&] is fine: the state lives until the message loop returns. In a function that returns a spec or a ui::Spec, it is not:
// Wrong: `clicks` dies when the function returns, the button keeps a dangling reference.
ui::Spec counter()
{
ui::Property<int> clicks{0};
return ui::Button{.text = "+", .on_click = [&] { clicks = clicks.get() + 1; }};
}
// Right: the state is passed in and outlives the window.
ui::Spec counter(ui::Property<int>& clicks)
{
return ui::HorizontalStack{
.spacing = 8,
.content = {
ui::Text{.text = ui::bind(clicks, [](int n) { return std::to_string(n); })},
ui::Button{.text = "+", .on_click = [&clicks] { clicks = clicks.get() + 1; }},
},
};
}
Capture by value, or capture a reference to state that outlives the window. Taking Property<T>& parameters makes the second one the natural thing to write.
build() Order¶
Every build() runs the same four steps:
- Allocate the control.
- Apply values and bindings: each field's setter is called once with its initial value, and a
bind(...)subscribes the control to its property. - Install handlers:
.on_*and.command. - Publish
.control.
Because handlers come after values, building a spec never fires a change handler for its own initial value. ui::CheckBox{.checked = true, .on_checked_changed = ...} builds a checked box without calling the handler; the handler runs for the first change after that.
Each binding's subscription belongs to the control that uses it, so it ends when the control is destroyed. The Property may be destroyed first as well; either side going away cuts the link.
Naming¶
ui::Button describes; ui::ButtonControl is the control. Both live in the one namespace, ui: a name ending in Control is the live object, the same name without it is the description you write. ui::Spec is the one "any piece of UI" value: what a component function returns, what a .content field and each entry of a .content list hold. ui::Element is the base of every node in the tree; keep those two apart.
A control's name is its spec's name plus Control (ui::Slider builds a ui::SliderControl). These are the pairs that do not follow that rule:
| Spec | Builds |
|---|---|
ui::VerticalStack, ui::HorizontalStack |
ui::StackControl (also reachable as ui::VerticalStackControl and ui::HorizontalStackControl) |
ui::Match, ui::Lazy, ui::State, ui::Shortcuts |
ui::ContentHost |
Constants are PascalCase, with no prefix: ui::Unbounded, ui::colors::White, ui::styles::Spaced, ui::AutoSize.
Item specs that only exist inside another control's spec (ui::Tab, ui::NavItem, ui::Section, ui::Cell) are documented on their parent control's page.
The same few shapes recur across controls, so a handler you have used on one control reads the same on the next:
| What | Shape |
|---|---|
| A two-state control | .checked, on_checked_changed(bool); std::optional<bool> where there is an indeterminate state (CheckBox, ToggleButton, AppBarToggleButton), and that one is one-way. |
| A selected index | std::optional<std::size_t>, unset for nothing selected, on_selection_changed(std::optional<std::size_t>); a control that always has one selected (Pager, Sections) passes a plain std::size_t. |
| A selected item of a typed view | on_selection_changed(std::optional<T>), a copy of the item, unset for nothing selected (ListView, TileView, TableView). |
| A selection with no single answer | on_selection_changed() (a text selection). |
| An event that can be stopped | The handler returns ui::Cancel to stop it, or ui::Proceed; one that returns nothing lets it go ahead. Every on_closing (Window, Dialog, the flyouts, InfoBar, TeachingTip, CommandBar), on_pane_closing, TableView's on_sorting, on_beginning_edit and on_cell_edited, AnnotatedScrollBar's on_scrolling, ComboBox's on_text_submitted, a view's on_drag_items_starting. Its args are input only. |
| A request a handler acts on | The handler returns ui::NotHandled to let it go on, or ui::Handled; one that returns nothing counts as handled. A shortcut's invoked, on_context_requested. Window::on_key_preview only watches keys pass by, so there a handler that returns nothing lets the key go on. |
| A number | double for values (Slider, NumberBox, Rating, ProgressBar); float for DIPs. |