Skip to content

Expander

Header: winui/ui/containers/Expander.hpp. Spec: winui/ui/containers/Expander.hpp.

A header row that shows or hides one piece of content below or above it.

The header is the control's single tab stop. A click on it or Space toggles expanded(). Enter doesn't, since it steps into the Expander when it is a focus scope. While collapsed, the content isn't part of the tree at all. It isn't arranged or drawn, and it is out of the tab order and the automation tree. Opening and closing slide the content out from behind the header, clipped, in 333 ms when opening and 167 ms when closing. The control's own height changes at once. set_content() takes ownership of the content and returns a non-owning observer.

Example:

// Declarative:
ui::Property<bool> open{false};
window.set_content(ui::Expander{
    .header             = "Advanced options",
    .expanded           = open,
    .vertical_alignment = ui::Alignment::Leading,
    .content            = ui::VerticalStack{
        .spacing  = 8,
        .content  = {
            ui::CheckBox{.text = "Verbose logging"},
            ui::CheckBox{.text = "Keep temporary files"},
        },
    },
});

// Imperative:
auto  stack    = std::make_unique<ui::StackControl>(ui::Orientation::Vertical);
auto* expander = stack->add(std::make_unique<ui::ExpanderControl>("Details"));
expander->set_content(std::make_unique<ui::TextControl>("Shown while expanded"));
expander->on_expanded_changed([&](bool now) { open = now; });
expander->set_expanded(true); // fires the handler

ui::Expander

A collapsible header over one child. Builds a ui::ExpanderControl.

.expanded = prop is two-way, which keeps the state in a model. When the user expands or collapses it, the property is written before on_expanded_changed runs.

Example:

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

window.set_content(ui::Expander{
    .header             = "Advanced options",
    .expanded           = open,
    .vertical_alignment = ui::Alignment::Leading,
    .content            = ui::VerticalStack{.spacing = 8, .content = {
        ui::CheckBox{.text = "Verbose logging"},
        ui::CheckBox{.text = "Experimental renderer"},
    }},
});

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

ui::Expander{
    .header, .expanded, .expand_direction, .content_padding, .style, .margin, .width, .height,
    .min_width, .min_height, .max_width, .max_height, .horizontal_alignment,
    .vertical_alignment, .visible, .tool_tip, .enabled, .accessibility, .on_expanded_changed,
    .on_pointer_entered, .on_pointer_exited, .on_got_focus, .on_lost_focus, .on_key_down,
    .on_wheel, .control, .header_content, .content
}

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

Field Type Default
header ui::Binding<std::string> The header text. See header_content for richer headers.
expanded ui::TwoWayField<bool> {false} Whether the content shows.
expand_direction ui::Binding<ui::ExpandDirection> {ui::ExpandDirection::Down} Down, the default, puts the content below the header, and Up above it.
content_padding ui::Binding<ui::Thickness> {ui::Thickness{16.0f}} The inset around the content inside the open content area, 16 DIPs by default (ui::ExpanderControl::set_content_padding()).
on_expanded_changed ui::Callback<void(bool)> Fires with the new state whenever it changes, through a header click, Space or a write to the bound property.
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::Expander> Out: the handle that receives the built control.
header_content ui::Spec Header content in place of the header text (ui::ExpanderControl::set_header_content()).
content ui::Spec The collapsible content.

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::ExpanderControl; the caller owns it.

ui::ExpanderControl

Derives from ui::Element, ui::ExpandCollapseProvider.

A header row that shows or hides one piece of content below or above it.

The header is the control's single tab stop. A click on it or Space toggles expanded(). Enter doesn't, since it steps into the Expander when it is a focus scope. While collapsed, the content isn't part of the tree at all. It isn't arranged or drawn, and it is out of the tab order and the automation tree. Opening and closing slide the content out from behind the header, clipped, in 333 ms when opening and 167 ms when closing. The control's own height changes at once. set_content() takes ownership of the content and returns a non-owning observer.

Example:

// Declarative:
ui::Property<bool> open{false};
window.set_content(ui::Expander{
    .header             = "Advanced options",
    .expanded           = open,
    .vertical_alignment = ui::Alignment::Leading,
    .content            = ui::VerticalStack{
        .spacing  = 8,
        .content  = {
            ui::CheckBox{.text = "Verbose logging"},
            ui::CheckBox{.text = "Keep temporary files"},
        },
    },
});

// Imperative:
auto  stack    = std::make_unique<ui::StackControl>(ui::Orientation::Vertical);
auto* expander = stack->add(std::make_unique<ui::ExpanderControl>("Details"));
expander->set_content(std::make_unique<ui::TextControl>("Shown while expanded"));
expander->on_expanded_changed([&](bool now) { open = now; });
expander->set_expanded(true); // fires the handler

ExpanderControl

ExpanderControl()

A collapsed Expander with an empty text header and no content.

ExpanderControl

ExpanderControl(std::string header, bool expanded = false)

An Expander with header as its text header, starting expanded or collapsed as expanded says. Doesn't fire on_expanded_changed().

const std::string& header() const

The header text, drawn in the header row unless header_content() replaces it.

set_header

void set_header(std::string header)

Changes the header text. Updates layout.

set_header_content

template <typename T>
T* set_header_content(std::unique_ptr<T> content)

Sets any element as header content, drawn in the header row in place of the text and sized like it. The row stretches it, so the element's own alignment places it within the row. It is part of the header button, so it gets no input of its own, and a click on it toggles the Expander. nullptr goes back to the text header. Returns a non-owning observer.

header_content

Element* header_content() const

The header content set with set_header_content(), or nullptr for a text header. Owned by the Expander.

expand_direction

ExpandDirection expand_direction() const

Which side the content opens on. Up puts the content above the header, rounds the header's bottom corners only while open, slides the content up from behind the header and turns the chevron around.

set_expand_direction

void set_expand_direction(ExpandDirection direction)

Changes the direction. Updates layout.

set_content

template <typename T>
T* set_content(std::unique_ptr<T> content)

Takes ownership of the collapsible content and returns a non-owning observer, like ScrollControl::set_child(). Replaces and destroys any previous content. nullptr clears it.

content

Element* content() const

The collapsible content, or nullptr before set_content(). Owned by the Expander.

expanded

bool expanded() const

Whether the content region is open or opening.

set_expanded

void set_expanded(bool expanded)

Opens or closes the content region and starts the slide. Fires on_expanded_changed() when the value changes. Updates layout.

on_expanded_changed

void on_expanded_changed(Callback<void(bool)> handler)

Fired whenever expanded changes, through a header click, Space or set_expanded().

content_padding

const Thickness& content_padding() const

The inset between the edges of the open content area and the content inside it, 16 DIPs by default. A dense list inside an Expander, such as a navigation pane, needs far less room than card-like content does.

set_content_padding

void set_content_padding(Thickness padding)

Changes the content padding. Updates layout.

header_bounds

const Rect& header_bounds() const

The header row's own rect from the most recent arrange(), for hit-testing the header.

content_bounds

const Rect& content_bounds() const

The clipped area the content slides into, from the most recent arrange(). Zero height while fully collapsed.

slide_progress

float slide_progress() const

How far the content is through the opening or closing slide, eased. 0 is fully hidden behind the header and 1 fully in place. Read it like FlyoutControl::transition_opacity().

The slide runs only in a host that ticks the control, such as ui::run()'s frame loop. Until the first tick, every set_expanded() jumps straight to the end value, so the content never stays stuck halfway behind the header.

focused

bool focused() const

Whether the header has focus. The header itself is the focusable element. The focusable elements inside content() join the tab order only while expanded (see collect_focusables()).

focusable

bool focusable() const override

The header is focusable while the Expander is enabled.

takes_focus_at

bool takes_focus_at(Point p) const override

Only a press on the header row focuses the Expander. Presses on the content don't.

collect_focusables

void collect_focusables(std::vector<Element*>& out) override

The header, then the focusable elements inside the content while the content region exists and the Expander isn't a focus scope.

is_pressed

bool is_pressed() const

Whether the header is drawn pressed, because the pointer is down on it or Space is held.

for_each_child

void for_each_child(ChildVisitor visit) const override

Visits the content, but only while the content region exists. A collapsed Expander's content is skipped by walks over the tree, as it is for layout, rendering and input.

automation_role

AutomationRole automation_role() const override

Exposed to UI Automation as a group.

automation_name_fallback

std::string automation_name_fallback() const override

The header text names the Expander when accessibility().name doesn't.

measure_override

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

The header row's height plus, while the content region exists, the content's desired height and the content padding.

arrange_override

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

Splits the bounds into the header row and the content area, as expand_direction() says, and places the content at its current slide offset.

pointer_override

bool pointer_override(const PointerEvent& e) override

A click on the header row toggles expanded(). A press reaches the content only inside the area it is drawn in (content_bounds()). Moves and releases always reach it.

hit_test_children

void hit_test_children(Point p, std::vector<Element*>& out) const override

The children p (in this element's layout coordinates) is tested against, topmost first. Default: what for_each_child() lists, last first. A container that draws in another order (Panel's z-index), a control with parts for_each_child() does not list, or one whose parts cover others in places (a pane drawn over the content) lists them here.

render_override

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

Draws the header row and chevron, the content clipped to its area, then the focus ring.

clear_dirty_override

void clear_dirty_override(DirtyFlags flags) override

Clears the dirty flags of this control, of the content while it exists and of the header content.

tick_override

void tick_override(Duration delta) override

Advances the open/close slide, then ticks the content while it exists.

collect_scope_focusables

void collect_scope_focusables(std::vector<Element*>& out) override

As a focus scope (Element::set_focus_scope(), off by default), the content leaves the window's tab order and gets a tab order of its own inside the Expander. Tab reaches the header, Enter steps into the content and Escape steps back out onto the header. Space toggles either way, so the keyboard can always collapse the group from its header.

enabled_changed

void enabled_changed() override

is_enabled() flipped, through set_enabled() here or on an ancestor, or a move to another parent. Drop any press or hover state. The element is redrawn either way. No-op by default.

focus_changed

void focus_changed(bool focused) override

Called by Window as focus reaches or leaves the header. Redraws.

key_override

bool key_override(const KeyEvent& e) override

Space toggles expanded() on key up. Enter does nothing here.

focus_ring_rect

Rect focus_ring_rect(const Theme&) const override

The focus ring goes around the header only.

focus_ring_corner_radius

float focus_ring_corner_radius(const Theme&) const override

The ring's corner radius: the theme's control radius by default.

Enumerations

ExpandDirection

enum class ExpandDirection

Which way an Expander opens.

Value
Down The default. The content opens below the header.
Up The content opens above the header, which stays at the bottom.

Tests

  • tests/test_expander.cpp