Skip to content

ComboBox

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

A combo box: a closed box showing one item of a list of strings, and a chevron that opens the list as a popup floating above the rest of the tree, so opening it never reflows the layout.

By default the box is read-only and the value is always one of the items (or nothing, showing the placeholder). A click, Space, F4 or Alt+Down opens the list. While open, hovering or Up/Down moves the highlight, and a click or Enter picks it and closes the list; Escape or a click outside closes it unchanged. While closed, Up/Down change the selection directly. Typing jumps to the next item starting with the typed letters; letters typed within a second of each other add to the search.

set_editable(true) turns the box into a text field the user can type any value into. Typing at the end of the text completes it to the first item it starts, with the completed part selected so the next letter replaces it. Enter or losing focus commits the text: text equal to an item picks it, other non-blank changed text raises on_text_submitted(). Escape reverts to the last committed text. Only F4 or Alt+Down open the list (Space is an ordinary character), and the list always shows every item (AutoSuggestBox filters as you type). The control stays one tab stop; typing and caret movement go to an internal TextBox, which draws its own caret and selection.

Example:

// Declarative: ui::ComboBox builds a ui::ComboBoxControl.
ui::Property<std::optional<std::size_t>> size{1};
window.set_content(ui::VerticalStack{
    .spacing  = 8,
    .content  = {
        ui::ComboBox{
            .header         = "Size",
            .items          = {"Small", "Medium", "Large"},
            .selected_index = size,
        },
        ui::Text{.text = ui::bind(size, [](std::optional<std::size_t> i) { return std::format("Index {}", i.value_or(0)); })},
    },
});

// Imperative and editable: any text goes, the items are suggestions.
auto combo = std::make_unique<ui::ComboBoxControl>(std::vector<std::string>{"Red", "Green", "Blue"});
combo->set_editable(true);
combo->set_header("Colors");
combo->set_placeholder_text("Pick a color");
combo->on_item_chosen([](std::size_t index) { /* index into items() */ });
combo->on_text_submitted([](const ui::ComboBoxTextSubmittedArgs& args) {
    return args.text.size() > 20 ? ui::Cancel : ui::Proceed; // too long: not committed
});
window.set_content(std::move(combo));

ui::ComboBox

A combo box: pick one of a list, or with editable type any value. Builds a ui::ComboBoxControl.

items is applied before text and selected_index, so a bound index always lands inside the list. When both are bound, text follows the selection.

Example:

ui::Property<std::optional<std::size_t>> size{1};
ui::Property<std::string>                color{"Blue"};

window.set_content(ui::VerticalStack{
    .spacing = 8,
    .content = {
        ui::ComboBox{
            .header         = "Size",
            .items          = {"Small", "Medium", "Large"},
            .selected_index = size,
            .width          = 200.0f,
        },
        ui::ComboBox{
            .header            = "Colors",
            .placeholder_text  = "Pick a color",
            .items             = {"Red", "Green", "Blue"},
            .text              = color,
            .editable          = true,
            .width             = 200.0f,
            .on_text_submitted = [](const ui::ComboBoxTextSubmittedArgs&) { return ui::Proceed; }, // free text
        },
    },
});

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

ui::ComboBox{
    .header, .description, .error, .placeholder_text, .items, .selected_index, .text,
    .editable, .style, .margin, .width, .height, .min_width, .min_height, .max_width,
    .max_height, .horizontal_alignment, .vertical_alignment, .visible, .tool_tip, .enabled,
    .accessibility, .on_text_changed, .on_item_chosen, .on_selection_changed,
    .on_text_submitted, .on_pointer_entered, .on_pointer_exited, .on_got_focus, .on_lost_focus,
    .on_key_down, .on_wheel, .control
}

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

Field Type Default
header ui::Binding<std::string> The label above the control. Empty draws none.
description ui::Binding<std::string> Secondary text under the box, such as what to enter or why; empty draws none. Screen readers offer it as help text.
error ui::Binding<std::string> What is wrong with the value: non-empty shows it in the description's place in the critical color and draws the border in that color, and screen readers report the field invalid. The field shows exactly what it is given, so bind it to a check of the value and decide there when it shows (after the first submit, say).
placeholder_text ui::Binding<std::string> The hint shown while nothing is selected or the text is empty ("Pick a color").
items ui::Binding<std::vector<std::string>> The choices, in display order. Bound, a new list replaces them and closes the list, as ui::ComboBoxControl::set_items() does.
selected_index ui::TwoWayField<std::optional<std::size_t>> The selected item; unset (the default) selects nothing and shows placeholder_text. Two-way with ui::bind(): picking, stepping or (editable) typing an item writes its index, other text writes unset.
text ui::TwoWayField<std::string> The text in the box: the selected item's, or when editable whatever was typed. Two-way with ui::bind().
editable ui::Binding<bool> {false} Whether the user can type a value of their own; false (the default) only picks items.
on_text_changed ui::Callback<void(const std::string&)> Editable: fires with the new text after each user edit (typing, Backspace, Delete).
on_item_chosen ui::Callback<void(std::size_t)> Fires with the index of the item the user picked from the list, even if the box already showed it.
on_selection_changed ui::Callback<void(std::optional<std::size_t>)> Fires with the new index when the selection changes, unset when it was cleared; not for a write that lands on the current item.
on_text_submitted ui::Callback<ui::Decision(const ui::ComboBoxTextSubmittedArgs&)> Editable: typed text matching no item, committed by Enter or focus loss. Return ui::Cancel to reject it (after putting the last good value back with set_text() through .control).
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::ComboBox> Out: the handle that receives the built control.

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

ui::ComboBoxControl

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

A combo box: a closed box showing one item of a list of strings, and a chevron that opens the list as a popup floating above the rest of the tree, so opening it never reflows the layout.

By default the box is read-only and the value is always one of the items (or nothing, showing the placeholder). A click, Space, F4 or Alt+Down opens the list. While open, hovering or Up/Down moves the highlight, and a click or Enter picks it and closes the list; Escape or a click outside closes it unchanged. While closed, Up/Down change the selection directly. Typing jumps to the next item starting with the typed letters; letters typed within a second of each other add to the search.

set_editable(true) turns the box into a text field the user can type any value into. Typing at the end of the text completes it to the first item it starts, with the completed part selected so the next letter replaces it. Enter or losing focus commits the text: text equal to an item picks it, other non-blank changed text raises on_text_submitted(). Escape reverts to the last committed text. Only F4 or Alt+Down open the list (Space is an ordinary character), and the list always shows every item (AutoSuggestBox filters as you type). The control stays one tab stop; typing and caret movement go to an internal TextBox, which draws its own caret and selection.

Example:

// Declarative: ui::ComboBox builds a ui::ComboBoxControl.
ui::Property<std::optional<std::size_t>> size{1};
window.set_content(ui::VerticalStack{
    .spacing  = 8,
    .content  = {
        ui::ComboBox{
            .header         = "Size",
            .items          = {"Small", "Medium", "Large"},
            .selected_index = size,
        },
        ui::Text{.text = ui::bind(size, [](std::optional<std::size_t> i) { return std::format("Index {}", i.value_or(0)); })},
    },
});

// Imperative and editable: any text goes, the items are suggestions.
auto combo = std::make_unique<ui::ComboBoxControl>(std::vector<std::string>{"Red", "Green", "Blue"});
combo->set_editable(true);
combo->set_header("Colors");
combo->set_placeholder_text("Pick a color");
combo->on_item_chosen([](std::size_t index) { /* index into items() */ });
combo->on_text_submitted([](const ui::ComboBoxTextSubmittedArgs& args) {
    return args.text.size() > 20 ? ui::Cancel : ui::Proceed; // too long: not committed
});
window.set_content(std::move(combo));

ComboBoxControl

ComboBoxControl()

An empty, read-only combo box (not focusable until it has items).

ComboBoxControl

ComboBoxControl(std::vector<std::string> items, std::optional<std::size_t> selected_index = std::nullopt)

A read-only combo box offering items. selected_index is clamped to the list; unset (the default) starts with nothing selected, showing the placeholder. Sets a 64 DIP minimum width.

editable

bool editable() const

Whether the user can type a value of their own. False (the default) only picks items.

set_editable

void set_editable(bool editable)

Switches between the read-only box and the text field. The text carries over: turning editing off keeps the selection, and drops typed text that matches no item. Updates the layout.

items

const std::vector<std::string>& items() const

The list's choices (UTF-8), in display order.

set_items

void set_items(std::vector<std::string> items)

Replaces the item list and closes the popup. A read-only box clears its selection, firing on_selection_changed() when something was selected; an editable one keeps its text, and the selection follows it to the new list.

selected_index

std::optional<std::size_t> selected_index() const

Index into items() of the selected item: the item the text matches exactly, when editable. Unset when nothing is selected.

set_selected_index

void set_selected_index(std::optional<std::size_t> index)

Shows item index, as picking it would, without raising on_item_chosen(). Read-only, the index is clamped to the list; editable, an index past the list is ignored. Unset clears the selection (and, editable, the text when it shows an item).

text

const std::string& text() const

The text in the box (UTF-8): the selected item's, or empty with nothing selected; when editable, whatever was typed or picked.

set_text

void set_text(std::string text)

Read-only, selects the item equal to text, or nothing when none is. Editable, replaces the text and moves the caret to the end without firing on_text_changed(), which is for user edits only. The new text is also the committed text Escape reverts to.

placeholder_text

const std::string& placeholder_text() const

The hint shown while nothing is selected or the text is empty; empty by default.

set_placeholder_text

void set_placeholder_text(std::string text)

Sets the hint ("Pick a color"). Updates the layout, since a read-only box fits its width to it.

const std::string& header() const

The label above the control. Empty by default.

set_header

void set_header(std::string header)

Sets the header. Empty draws none and takes no room. A change updates the layout.

description

const std::string& description() const

The secondary text under the box; empty (the default) draws none.

set_description

void set_description(std::string description)

Sets the description. Empty draws none and takes no room. A change updates the layout.

error

const std::string& error() const

What is wrong with the value, shown in the description's place in the critical color, with the border in the same color; empty (the default) shows the description.

set_error

void set_error(std::string error)

Sets the error. A change updates the layout and redraws.

automation_help_text_fallback

std::string automation_help_text_fallback() const override

The description, or the error while one is set, is the help text a screen reader offers when accessibility().description is empty.

automation_invalid

bool automation_invalid() const override

Invalid while an error is set.

automation_name_fallback

std::string automation_name_fallback() const override

The header names the control when accessibility().name does not.

on_selection_changed

void on_selection_changed(Callback<void(std::optional<std::size_t>)> handler)

Fires when the selection changes: an item picked, Up/Down or typing while closed, text typed or set that matches another item or none, set_items() or set_selected_index(). Unset when it was cleared; not for a write that lands on the current item.

on_item_chosen

void on_item_chosen(Callback<void(std::size_t)> handler)

Fires when the user picks an item from the open list (a click, or Enter on the highlighted item), even if the box already showed it, and when an editable box commits typed text equal to an item. Receives the index into items(); the box already shows the item when it fires.

on_text_changed

void on_text_changed(Callback<void(const std::string&)> handler)

Editable only: fires with the new text after any user-driven text edit (typing, Backspace, Delete, UI Automation's SetValue), once completion has filled it in. Not fired after set_text() or when an item is picked (that is on_item_chosen()). Replaces any previous handler.

on_text_submitted

void on_text_submitted(Callback<Decision(const ComboBoxTextSubmittedArgs&)> handler)

Editable only: fires when typed text matching no item is committed (see the class comment). Return ui::Cancel to reject the text, typically after putting the last good value back with set_text(); the text is then not committed.

expanded

bool expanded() const

Whether the item list is open. Opened by the user or through UI Automation's ExpandCollapse; never opens while items() is empty, and there is no public setter.

focused

bool focused() const

Whether the control (and so, editable, its inner TextBox) holds keyboard focus.

focusable

bool focusable() const override

True while enabled and holding at least one item; an editable box needs no items, since it is still a free-text field.

for_each_child

void for_each_child(ChildVisitor visit) const override

Lists the internal TextBox while editable, so it is updated, animated and drawn along with the combo box.

collect_focusables

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

One tab stop: the internal TextBox is part of this control, not a stop of its own.

automation_children

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

The internal TextBox is part of this node and not listed. While the list is open, a List node of the items, each a ListItem a client can select.

automation_focus

Element* automation_focus() const override

The highlighted item while the list is open, so a screen reader reads it as the arrow keys move.

handle_context_menu

bool handle_context_menu(std::optional<Point> position, ContextMenuHost& host) override

Editable: the inner TextBox's context menu, Cut/Copy/Paste/Select All only, with no spelling section. Read-only: none.

cursor_at

std::optional<CursorShape> cursor_at(Point p) const override

Editable: the text field's cursor over the text field (an I-beam, an arrow over its buttons).

automation_role

AutomationRole automation_role() const override

A ComboBox, with its text as the Value pattern (read-only unless editable) and ExpandCollapse for open and close.

caret_bounds

std::optional<Rect> caret_bounds() const override

Editable: the inner text box's caret.

visual_state

ButtonState visual_state() const

The Normal/Hover/Pressed/Disabled state the control draws in: the whole closed box when read-only, the chevron button when editable.

measure_override

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

Desired width fits the widest item, and the placeholder (read-only) or the inner TextBox's own desired width (editable), so the box doesn't reflow as the selection changes. Desired height is the header plus one row, open or closed, since the list floats as a popup.

arrange_override

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

Places the header, and when editable the inner TextBox beside the chevron column.

pointer_override

bool pointer_override(const PointerEvent& e) override

A click on the box (read-only) or on the chevron (editable) toggles the list; anything else in an editable box goes to the TextBox. Clicks on the open list reach the popup.

render_override

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

Draws the header, the box with its text (or placeholder) and the chevron. The open list is a popup the window draws.

tick_override

void tick_override(Duration delta) override

Ticks the inner TextBox, runs the list's slide-open, where the theme has one (Theme::popup_motion.list), and times out the typed search.

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; mirrored into the inner TextBox. Losing focus closes the list, and an editable box commits its typed text.

key_override

bool key_override(const KeyEvent& e) override

F4 or Alt+Down/Up toggles the list; Up/Down/Enter/Escape drive it while open. Read-only, Space toggles it and Up/Down step the selection while closed. Editable, Enter commits and Escape reverts while closed, and everything else edits the text.

text_override

bool text_override(const TextInputEvent& text_event) override

Typed text searches the items (read-only) or goes to the inner TextBox (editable).

focus_ring_rect

Rect focus_ring_rect(const Theme& theme) const override

Read-only: around the box; inside the classic field, round the selected text, while the list is closed.

focus_ring_corner_radius

float focus_ring_corner_radius(const Theme& theme) const override

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

draws_focus_ring

bool draws_focus_ring() const override

Read-only only: an editable box shows focus with its accent border.

struct ComboBoxTextSubmittedArgs

What a ComboBox's on_text_submitted gets: the typed text.

Field Type Default
text std::string The committed text, which matches no item.

Tests

  • tests/test_combo_box.cpp
  • tests/test_combo_box_read_only.cpp