Skip to content

Match

Header: winui/ui/hosting/ContentHost.hpp. Spec: winui/ui/spec/Dynamic.hpp.

A node that owns exactly one child and can replace it at runtime. Layout, rendering, input, focus and animation all pass through to the child, so wrapping a subtree in one changes nothing visible about it.

It has two uses. The first is a swappable slot for UI whose structure varies (a wizard page, a master/detail pane): ui::Match builds one and rebuilds its child when a Property changes. The second is a base class for a control made of one child plus its own chrome (a card, a padded group). Unlike most controls it is not final: override measure_override(), arrange_override() and render_override(), and call the ContentHost version for the child. Border and DensityScope derive from it.

Example:

// A reusable card: padding plus a rounded background, the child forwarded.
class Card final : public ui::ContentHost
{
public:
    explicit Card(std::unique_ptr<ui::Element> child)
    {
        set_child(std::move(child));
    }

protected:
    ui::Size measure_override(ui::Size available, const ui::MeasureContext& context) const override
    {
        const ui::Size inner = ui::ContentHost::measure_override(
            ui::Size{std::max(0.0f, available.width - 32.0f), std::max(0.0f, available.height - 32.0f)},
            context);
        return ui::Size{inner.width + 32.0f, inner.height + 32.0f};
    }
    void arrange_override(const ui::Rect& final_rect, const ui::ArrangeContext& context) override
    {
        ui::ContentHost::arrange_override(final_rect.inset(ui::Thickness{16.0f}), context);
    }
    void render_override(ui::Renderer& renderer, const ui::RenderContext& context) const override
    {
        const ui::Theme& theme = context.theme;
        renderer.fill_rounded_rect(bounds(), 8.0f, theme.colors().background);
        ui::ContentHost::render_override(renderer, context); // then the child
    }
};

// As a runtime-swappable slot:
auto stack = std::make_unique<ui::StackControl>(ui::Orientation::Vertical);
ui::ContentHost* slot = stack->add(std::make_unique<ui::ContentHost>());
slot->set_child(std::make_unique<Card>(std::make_unique<ui::TextControl>("Step 1")));
ui::ButtonControl* next = stack->add(std::make_unique<ui::ButtonControl>("Next"));
next->on_click([slot] { slot->set_child(std::make_unique<ui::TextControl>("Step 2")); });
window.set_content(std::move(stack));

ui::Match

Structure driven by a value: one node showing the subtree that belongs to the current value. Builds a ui::ContentHost.

.value is what to look at, usually ui::bind(prop), and .content turns it into the subtree to show. The subtree is built once at construction and again on every change of the value. The Property's equality gate drops writes that change nothing, so a redundant assignment costs nothing. Only this node's subtree is rebuilt, so the cost grows with the subtree, not with the screen.

By default a change destroys the old subtree, so what the user typed or scrolled in it is gone and handles published from inside it through .control dangle afterwards. That is right for pages whose state should reset. .keep = true keeps every subtree it has built and only hides the ones not current, so tab pages and master/detail panes come back as they were left.

Name the value's type, ui::Match<Page>{.value = page, ...}: C++ deduces a spec's type only from the field's own type, which a Property is not.

LIFETIME: .content is stored on the built node and invoked for the life of the tree, so it must not capture anything shorter-lived than the window. Capturing a reference to model state the caller owns is the intended shape. A bound Property that dies first stops the rebuilds.

Example:

enum class Page { Home, Settings };
ui::Property<Page> page{Page::Home};

window.set_content(ui::VerticalStack{.content = {
    ui::HorizontalStack{.spacing = 8, .content = {
        ui::Button{.text = "Home",     .on_click = [&] { page = Page::Home; }},
        ui::Button{.text = "Settings", .on_click = [&] { page = Page::Settings; }},
    }},
    ui::Match<Page>{
        .value   = page,
        .keep    = true, // the check box is still ticked after a visit to Home
        .content = [](const Page& p) -> ui::Spec
        {
            switch (p)
            {
            case Page::Home:     return ui::Text{.text = "Welcome home"};
            case Page::Settings: return ui::CheckBox{.text = "Dark mode"};
            }
            return {};
        },
    },
}});

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

ui::Match{
    .value, .keep, .content_transitions, .style, .margin, .width, .height, .min_width,
    .min_height, .max_width, .max_height, .horizontal_alignment, .vertical_alignment, .visible,
    .tool_tip, .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
value ui::Binding<T> The value to match on: a plain value (built once) or ui::bind(prop), which shows the content for the new value whenever the property changes.
keep bool false false: a change destroys the old subtree and builds the new one. true: each value's subtree is built the first time the value is seen and kept until the Match goes, hidden while another one shows, so it keeps its state. One subtree is kept per distinct value, which suits a handful of pages; for a value with no bound on how many there are, use a ui::ForEach or a view instead.
content_transitions ui::Binding<ui::TransitionList> Transitions for a change of content: with a ui::ContentTransition the new subtree slides and fades in while the old one fades out. Not used with keep.
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<std::type_identity_t<ui::Match>> Out: the handle that receives the built control. Named through std::type_identity_t so that passing a handle does not take part in deducing the type argument.
content ui::Callback<ui::Spec(const std::type_identity_t<T>&)> Produces the subtree for a given value. Returning an empty Spec is legal and means "show nothing". Its parameter names T through std::type_identity_t so that .value alone decides T when the type argument is left out.

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

build

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

Consumes the spec and returns the built ui::ContentHost, the subtree for the current value already built; the caller owns it.

ui::Lazy

A subtree that is built the first time it is shown. Builds a ui::ContentHost.

.visible on any spec hides a subtree that already exists. Lazy is the same thing for a subtree that is expensive to build and that most users never open: .content is a function, and it runs the first time .visible is true. After that the subtree is hidden and shown again without being rebuilt, so it keeps its state. While hidden the Lazy takes no room.

.unload = true destroys the subtree whenever .visible turns false and builds it again on the next true: the memory is given back, and the state is lost.

LIFETIME: .content is stored on the built node, so it must not capture anything shorter-lived than the window.

Example:

ui::Spec log_view()
{
    return ui::Text{.text = "Started."};
}

ui::Property<bool> show_log{false};

window.set_content(ui::VerticalStack{.content = {
    ui::ToggleSwitch{.header = "Show the log", .checked = show_log},
    ui::Lazy{
        .visible = show_log,
        .content = []() -> ui::Spec { return log_view(); }, // not built until switched on
    },
}});

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

ui::Lazy{
    .visible, .unload, .content_transitions, .style, .margin, .width, .height, .min_width,
    .min_height, .max_width, .max_height, .horizontal_alignment, .vertical_alignment,
    .tool_tip, .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
unload bool false true: destroy the subtree when visible turns false, instead of hiding it.
content_transitions ui::Binding<ui::TransitionList> Transitions for the content: with a ui::ContentTransition it slides and fades in when it is built and, with unload, fades out when it goes.
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::Lazy> Out: the handle that receives the built control.
content ui::Callback<ui::Spec()> Builds the subtree: once, the first time visible is true (with unload, each time it turns true). Never called while visible stays false.

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

build

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

Consumes the spec and returns the built ui::ContentHost, empty until visible is true; the caller owns it.

ui::State

State that belongs to a piece of UI instead of to the app. Builds a ui::ContentHost that owns an S and shows what .content makes of it.

A component that needs a private flag, a text being edited or a small struct of Properties has nowhere to keep it: a local variable is gone when the function returns, and the handlers that captured it dangle. State gives it a home in the tree. The node constructs one S, passes it to .content by reference, once, and destroys it after the subtree. So a handler or a binding inside the subtree may capture that reference: it lives exactly as long as they do.

.content runs once. It is not run again when the state changes; what changes on screen is what was bound to the state. S is usually a ui::Property or a struct of them. State shared between components still lives in the caller and is passed in as Property<T>&.

.initial returns the starting state. It is a function because a Property cannot be moved; left out, the state is S{}.

Example:

// A disclosure row whose "open" flag belongs to the row, not to the app.
ui::Spec disclosure(std::string title, ui::Callback<ui::Spec()> body)
{
    return ui::State<ui::Property<bool>>{
        .content = [title = std::move(title), body = std::move(body)](ui::Property<bool>& open) mutable -> ui::Spec
        {
            return ui::VerticalStack{.content = {
                ui::Button{.text = std::move(title), .on_click = [&open] { open.toggle(); }},
                ui::Lazy{.visible = open, .content = std::move(body)},
            }};
        },
    };
}

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

ui::State{
    .initial, .style, .margin, .width, .height, .min_width, .min_height, .max_width,
    .max_height, .horizontal_alignment, .vertical_alignment, .visible, .tool_tip,
    .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
initial ui::Callback<S()> Makes the starting state, constructed in place in the node; S{} when left out.
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::State> Out: the handle that receives the built control.
content ui::Callback<ui::Spec(S&)> Builds the subtree from the state, once. The reference stays valid for as long as the subtree lives, so handlers and bindings inside may capture it.

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

build

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

Consumes the spec and returns the built node, a ui::ContentHost owning the state and the subtree made from it; the caller owns it.

ui::ContentHost

Derives from ui::Element.

A node that owns exactly one child and can replace it at runtime. Layout, rendering, input, focus and animation all pass through to the child, so wrapping a subtree in one changes nothing visible about it.

It has two uses. The first is a swappable slot for UI whose structure varies (a wizard page, a master/detail pane): ui::Match builds one and rebuilds its child when a Property changes. The second is a base class for a control made of one child plus its own chrome (a card, a padded group). Unlike most controls it is not final: override measure_override(), arrange_override() and render_override(), and call the ContentHost version for the child. Border and DensityScope derive from it.

Example:

// A reusable card: padding plus a rounded background, the child forwarded.
class Card final : public ui::ContentHost
{
public:
    explicit Card(std::unique_ptr<ui::Element> child)
    {
        set_child(std::move(child));
    }

protected:
    ui::Size measure_override(ui::Size available, const ui::MeasureContext& context) const override
    {
        const ui::Size inner = ui::ContentHost::measure_override(
            ui::Size{std::max(0.0f, available.width - 32.0f), std::max(0.0f, available.height - 32.0f)},
            context);
        return ui::Size{inner.width + 32.0f, inner.height + 32.0f};
    }
    void arrange_override(const ui::Rect& final_rect, const ui::ArrangeContext& context) override
    {
        ui::ContentHost::arrange_override(final_rect.inset(ui::Thickness{16.0f}), context);
    }
    void render_override(ui::Renderer& renderer, const ui::RenderContext& context) const override
    {
        const ui::Theme& theme = context.theme;
        renderer.fill_rounded_rect(bounds(), 8.0f, theme.colors().background);
        ui::ContentHost::render_override(renderer, context); // then the child
    }
};

// As a runtime-swappable slot:
auto stack = std::make_unique<ui::StackControl>(ui::Orientation::Vertical);
ui::ContentHost* slot = stack->add(std::make_unique<ui::ContentHost>());
slot->set_child(std::make_unique<Card>(std::make_unique<ui::TextControl>("Step 1")));
ui::ButtonControl* next = stack->add(std::make_unique<ui::ButtonControl>("Next"));
next->on_click([slot] { slot->set_child(std::make_unique<ui::TextControl>("Step 2")); });
window.set_content(std::move(stack));

ContentHost

ContentHost() = default

An empty host; it measures to nothing until set_child().

set_child

template <typename T>
T* set_child(std::unique_ptr<T> child)

Takes ownership of the child and returns a non-owning observer, the same convention as StackControl::add(). Replaces and destroys the previous child, then updates the layout and redraws. A null child leaves the host empty, like clear_child().

With a ContentTransition among the children_transitions() the change is animated: the new child slides and fades in while the old one fades out over it. The old child is out of layout, input and the tab ring at once, and destroyed when its exit has played.

Safe to call from a handler of the child being replaced: while a Window is dispatching, the old child is destroyed only once that dispatch has returned.

clear_child

void clear_child()

Destroys the current child, leaving the host empty (it then measures to nothing). Updates the layout and redraws.

orphan_child

std::unique_ptr<Element> orphan_child()

Takes the child out alive and hands it back, leaving the host empty, for adding it somewhere else. Focus inside it is cleared and it plays no exit transition. Null when the host is empty. Updates the layout and redraws.

child

Element* child() const

The current child as a non-owning observer, or nullptr when empty.

hit_test_transparent

bool hit_test_transparent() const override

True while the host has no child, or its child is not Visible: the pointer then reaches what is behind the host. A host that draws something of its own (a background, a header) overrides this to return false.

for_each_child

void for_each_child(ChildVisitor visit) const override

Lists the child, if any.

measure_override

Size measure_override(Size available, const MeasureContext& context) const override

The child's desired size, or zero when empty. A subclass adding chrome calls this with the space left over, then adds its own.

arrange_override

void arrange_override(const Rect& final_rect, const ArrangeContext& context) override

Gives the child the whole of final_rect; a subclass passes the inset rect instead.

render_override

void render_override(Renderer& renderer, const RenderContext& context) const override

Renders the child; a subclass draws its chrome first, then calls this.

tick_override

void tick_override(Duration delta) override

Advances the exit animation of a replaced child.

Tests

  • tests/test_spec.cpp
  • tests/test_dynamic.cpp