Skip to content

SplitButton

Header: winui/ui/input/SplitButton.hpp. Spec: winui/ui/input/SplitButton.hpp.

A button with a second, chevron part that opens a flyout.

The primary part raises on_click() and runs the command; the 35 DIP secondary part at the right (or Alt+Down / F4) opens the flyout below the button. As for every flyout anchor, the flyout owns the split button: make it the flyout's anchor and the flyout wires itself in through set_flyout(). The declarative form is ui::SplitButton, usually as a ui::Flyout's .anchor.

Example:

ui::Property<ui::Color> current{ui::colors::Black};
ui::Property<ui::Color> ink{ui::colors::Black};

window.set_content(ui::Flyout{
    .anchor  = ui::SplitButton{ .text     = "Choose color",
                                .on_click = [&] { ink = current.get(); } },
    .content = ui::HorizontalStack{ .spacing = 4, .content = {
        ui::Button{ .text = "Red",  .on_click = [&] { current = ui::Color::from_argb(0xFFFF0000); } },
        ui::Button{ .text = "Blue", .on_click = [&] { current = ui::Color::from_argb(0xFF0000FF); } },
    }},
});

// Imperatively:
auto  flyout = std::make_unique<ui::FlyoutControl>();
auto* split  = flyout->set_anchor(std::make_unique<ui::SplitButtonControl>("Choose color"));
flyout->set_content(std::make_unique<ui::TextControl>("Pick a color"));
split->on_click([&] { ink = current.get(); });

ui::SplitButton

A button with a second part that opens a flyout. Builds a ui::SplitButtonControl.

The primary part raises .on_click and runs .command; the secondary part (the chevron) opens the flyout the split button is the .anchor of, below it and left-aligned.

Example:

ui::Property<std::string> current{"Red"};
ui::Property<std::string> ink{"Black"};

window.set_content(ui::Flyout{
    .anchor  = ui::SplitButton{.text     = "Choose color",
                               .on_click = [&] { ink = current.get(); }},
    .content = ui::VerticalStack{.content = {
        ui::Button{.text = "Red", .on_click = [&] { current = "Red"; }},
        ui::Button{.text = "Blue", .on_click = [&] { current = "Blue"; }},
    }},
});

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

ui::SplitButton{
    .text, .padding, .style, .margin, .width, .height, .min_width, .min_height, .max_width,
    .max_height, .horizontal_alignment, .vertical_alignment, .visible, .tool_tip, .enabled,
    .accessibility, .command, .command_parameter, .on_click, .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
text ui::Binding<std::string> The primary part's label, ignored while .content is set.
padding ui::Binding<std::optional<ui::Thickness>> Padding around the primary part's content; nullopt keeps the theme's button padding.
command ui::CommandRef Run after .on_click; while it is disabled the primary part is too (the secondary part still opens the flyout).
command_parameter std::string Handed to .command when it runs.
on_click ui::Action A click on the primary part. The chevron raises nothing, it opens the flyout.
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::SplitButton> Out: the handle that receives the built control.
content ui::Spec The primary part's content, instead of .text.

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

ui::ToggleSplitButton

A split button whose primary part also toggles checked. Builds a ui::ToggleSplitButtonControl.

The chevron opens the flyout it is the .anchor of, as ui::SplitButton's does. .checked is two-way with a Property<bool> or a Property<std::optional<bool>>: the button shows the property and a click on the primary part writes it back, before .on_checked_changed runs.

Example:

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

window.set_content(ui::MenuFlyout{
    .items   = {{.text = "Bullets"}, {.text = "Numbers"}},
    .anchor  = ui::ToggleSplitButton{
        .text                  = "List",
        .checked               = bullets,
    },
});

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

ui::ToggleSplitButton{
    .text, .checked, .padding, .style, .margin, .width, .height, .min_width, .min_height,
    .max_width, .max_height, .horizontal_alignment, .vertical_alignment, .visible, .tool_tip,
    .enabled, .accessibility, .command, .command_parameter, .on_click, .on_checked_changed,
    .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
text ui::Binding<std::string> The primary part's label, ignored while .content is set.
checked ui::CheckedField {false} Whether the button is checked; std::nullopt shows unchecked, since the button has no third state. Seeding it raises no .on_checked_changed. Two-way with a Property<bool> or a Property<std::optional<bool>>. See ui::CheckedField.
padding ui::Binding<std::optional<ui::Thickness>> Padding around the primary part's content; nullopt keeps the theme's button padding.
command ui::CommandRef As ui::SplitButton's: run after .on_click; the primary part disabled while it is.
command_parameter std::string Handed to .command when it runs.
on_click ui::Action A click on the primary part.
on_checked_changed ui::Callback<void(bool)> The checked state changed: a click on the primary part, UI Automation's Toggle, or a bound value.
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::ToggleSplitButton> Out: the handle that receives the built control.
content ui::Spec The primary part's content, instead of .text.

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

ui::SplitButtonControl

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

A button with a second, chevron part that opens a flyout.

The primary part raises on_click() and runs the command; the 35 DIP secondary part at the right (or Alt+Down / F4) opens the flyout below the button. As for every flyout anchor, the flyout owns the split button: make it the flyout's anchor and the flyout wires itself in through set_flyout(). The declarative form is ui::SplitButton, usually as a ui::Flyout's .anchor.

Example:

ui::Property<ui::Color> current{ui::colors::Black};
ui::Property<ui::Color> ink{ui::colors::Black};

window.set_content(ui::Flyout{
    .anchor  = ui::SplitButton{ .text     = "Choose color",
                                .on_click = [&] { ink = current.get(); } },
    .content = ui::HorizontalStack{ .spacing = 4, .content = {
        ui::Button{ .text = "Red",  .on_click = [&] { current = ui::Color::from_argb(0xFFFF0000); } },
        ui::Button{ .text = "Blue", .on_click = [&] { current = ui::Color::from_argb(0xFF0000FF); } },
    }},
});

// Imperatively:
auto  flyout = std::make_unique<ui::FlyoutControl>();
auto* split  = flyout->set_anchor(std::make_unique<ui::SplitButtonControl>("Choose color"));
flyout->set_content(std::make_unique<ui::TextControl>("Pick a color"));
split->on_click([&] { ink = current.get(); });

SplitButtonControl

SplitButtonControl()

An empty split button: no text or content, no flyout.

SplitButtonControl

SplitButtonControl(std::string text)

A split button whose primary part shows text.

~SplitButtonControl

~SplitButtonControl() override

text

const std::string& text() const

The primary part's text, used while there is no content().

set_text

void set_text(std::string text)

Changes the text, which is also the accessible name when none is set. The button is measured again and redrawn.

set_content

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

The primary part's content, in place of text(). Returns the observer.

content

Element* content() const

The primary part's content (owned by the button), or null.

set_padding

void set_padding(std::optional<Thickness> padding)

Padding around the primary part's content; nullopt keeps the theme's button padding.

primary_enabled

bool primary_enabled() const

Whether the primary part takes a click: enabled, and the command (if any) is too.

on_click

void on_click(Callback<void()> handler)

The primary part was clicked (or Space/Enter released on the control).

set_command

void set_command(CommandRef command, std::string parameter = {})

A command and its parameter, run after on_click. The primary part is disabled while the command is, following it as it changes, and without a tooltip of its own the button shows the command's description. A destroyed command leaves the button as if it had none.

command

const Command* command() const

The command set_command() installed, or null.

tool_tip

std::string tool_tip() const override

The tooltip set on the button, else its command's description.

set_flyout

void set_flyout(FlyoutBase* flyout)

The flyout the chevron opens. Set for you when the split button becomes a flyout's anchor.

flyout

FlyoutBase* flyout() const

The flyout the chevron part opens (non-owning; it owns this button), or null.

is_flyout_open

bool is_flyout_open() const

Whether the flyout is showing.

open_flyout

void open_flyout()

Shows the flyout below the button, left edges lined up.

close_flyout

void close_flyout()

Hides the flyout if it is open.

checked

virtual bool checked() const

A toggle split button's checked state; always false for a plain SplitButton.

primary_rect

Rect primary_rect() const

The primary part, without the divider, from the last arrange.

secondary_rect

Rect secondary_rect() const

The chevron part, 35 DIPs wide at the right, from the last arrange.

chevron

AnimatedIconControl* chevron() const

The chevron (an AnimatedIcon).

focused

bool focused() const

Whether the control has keyboard focus.

focusable

bool focusable() const override

One tab stop for both parts, while enabled.

automation_role

AutomationRole automation_role() const override

A SplitButton.

automation_name_fallback

std::string automation_name_fallback() const override

The text, when no accessible name is set.

visual_state

SplitButtonState visual_state() const

The visual state the button is drawn in, derived from hover, press, key and flyout state.

measure_override

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

The primary part (content or text plus padding, at least 35 DIPs wide), the 1 DIP divider and the 35 DIP chevron part side by side.

arrange_override

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

Splits final_rect into the two parts and arranges the content in the primary one.

render_override

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

Draws each part in its own state's colors, the divider, the content and the chevron.

pointer_override

bool pointer_override(const PointerEvent& e) override

Tracks hover and press per part; a click on the primary part clicks, on the chevron opens.

tick_override

void tick_override(Duration delta) override

Ticks the content and the chevron's animation.

on_click_primary

virtual void on_click_primary()

Raises on_click, then runs the command. ToggleSplitButtonControl toggles first.

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.

key_override

bool key_override(const KeyEvent& e) override

Space/Enter click the primary part on key up; Alt+Down and F4 open the flyout.

focus_changed

void focus_changed(bool focused) override

Called by the Window as focus arrives or leaves; redraws.

focus_ring_corner_radius

float focus_ring_corner_radius(const Theme& theme) const override

The ring follows the button's corner radius.

automation_providers

void automation_providers(AutomationProviders& out) override

Lists what a client can do with this node: set out's field for each pattern this control implements now to this, through the provider interface it inherits privately (ui/Automation.hpp). A pattern that only applies in some states is an if. out arrives empty, and nothing is listed by default.

Example:

class Chip final : public ui::Element, private ui::InvokeProvider
{
protected:
    void automation_providers(ui::AutomationProviders& out) override
    {
        out.invoke = this;
    }

private:
    bool invoke() override
    {
        on_click_();
        return true;
    }
    ui::Callback<void()> on_click_;
};

class ToggleSplitButtonControl

Derives from ui::SplitButtonControl, ui::ToggleProvider.

A SplitButton whose primary part also toggles checked(). Checked, both parts draw in the accent colors. The declarative form is ui::ToggleSplitButton.

Example:

ui::Property<bool> bullets{false};
ui::Property<int>  bullet_style{0};

window.set_content(ui::Flyout{
    .anchor  = ui::ToggleSplitButton{ .text    = "Bullets",
                                      .checked = bullets }, // two-way
    .content = ui::VerticalStack{ .content = {
        ui::Button{ .text = "Round", .on_click = [&] { bullet_style = 0; } },
        ui::Button{ .text = "Square", .on_click = [&] { bullet_style = 1; } },
    }},
});

ToggleSplitButtonControl

ToggleSplitButtonControl() = default

Unchecked, with no text.

ToggleSplitButtonControl

ToggleSplitButtonControl(std::string text)

Unchecked, the primary part showing text.

checked

bool checked() const override

Whether the button is checked.

set_checked

void set_checked(bool checked)

Sets the checked state; fires on_checked_changed() only when the value changes.

on_checked_changed

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

Fires with the new state after a click or set_checked() changes it.

on_click_primary

void on_click_primary() override

Toggles, then raises the base's on_click and runs the command.

Enumerations

SplitButtonState

enum class SplitButtonState

The visual state a SplitButton draws in. A checked ToggleSplitButton draws the same states in the accent colors.

Value
Normal At rest.
Disabled is_enabled() is false.
FlyoutOpen The flyout is open: both parts drawn pressed.
TouchPressed Space or Enter held: both parts pressed.
PrimaryPointerOver The pointer is over the primary part.
PrimaryPressed The primary part is pressed.
SecondaryPointerOver The pointer is over the chevron part.
SecondaryPressed The chevron part is pressed.

Tests

  • tests/test_split_button.cpp