Skip to content

Tabs

Header: winui/ui/navigation/Tabs.hpp. Spec: winui/ui/navigation/Tabs.hpp.

A strip of tabs over the selected tab's content.

The view owns every tab's content but only measures, arranges and renders the selected tab's content. Each tab has a key (Tab::key, else its header) that the events report and that stays with it when it is dragged elsewhere. The close button, a middle click and Ctrl+F4 remove the tab and select its neighbor. With an on_tab_close_requested() handler they raise that instead, and the handler decides, calling remove_tab() to close it. The "+" button shows once there is an on_add_tab_button_click() handler, unless set_add_tab_button_visible() says otherwise. Tabs that do not fit scroll, and dragging a tab reorders the strip. The declarative form is ui::Tabs.

Example:

auto  owned = std::make_unique<ui::TabsControl>();
auto* tabs  = owned.get();
tabs->add_tab("Home", std::make_unique<ui::TextControl>("Start here"));
tabs->add_tab(ui::Tab{ .header  = "Document 1",
                       .key     = "doc-1",
                       .icon    = ui::IconGlyph{0xE8A5},
                       .content = std::make_unique<ui::TextControl>("Hello") });
tabs->set_closable(0, false);
tabs->on_add_tab_button_click([tabs] {
    tabs->add_tab("New document", std::make_unique<ui::TextControl>("Empty"));
    tabs->set_selected_index(tabs->tab_count() - 1);
});
tabs->on_selection_changed([](const std::string& key) { std::println("tab {}", key); });
window.set_content(std::move(owned));

ui::Tabs

A tab strip over swappable content. Builds a ui::TabsControl. The first tab is selected unless .selected_key names another. A tab's close button removes it, unless .on_tab_close_requested is set. Then the handler decides, and closes the tab by calling remove_tab() through .control. The "+" shows once .on_add_tab_button_click is set.

Example:

ui::Control<ui::Tabs>     tabs;
ui::Property<std::string> current;
int                       documents = 1;

window.set_content(ui::Tabs{
    .selected_key            = current, // "home", then whichever tab the user picks
    .tab_width_mode          = ui::TabsWidthMode::SizeToContent,
    .on_tab_close_requested  = [&](const std::string& key) { tabs->remove_tab(key); },
    .on_add_tab_button_click = [&] {
        tabs->add_tab(ui::Tab{.header = "New document", .key = std::format("doc-{}", ++documents)});
        tabs->set_selected_index(tabs->tab_count() - 1);
    },
    .control = tabs,
    .items   = {
        ui::Tab{ .header = "Home", .key = "home", .closable = false, .content = ui::Text{ .text = "Start here" } },
        ui::Tab{ .header = "Document 1", .key = "doc-1", .content = ui::Text{ .text = "Hello" } },
    },
});

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

ui::Tabs{
    .selected_key, .tab_width_mode, .close_button_overlay_mode, .add_tab_button_visible,
    .can_reorder_tabs, .strip_background, .style, .margin, .width, .height, .min_width,
    .min_height, .max_width, .max_height, .horizontal_alignment, .vertical_alignment, .visible,
    .tool_tip, .enabled, .accessibility, .on_selection_changed, .on_tab_close_requested,
    .on_add_tab_button_click, .on_tabs_changed, .on_pointer_entered, .on_pointer_exited,
    .on_got_focus, .on_lost_focus, .on_key_down, .on_wheel, .control, .items,
    .tab_strip_header_content, .tab_strip_footer_content
}

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

Field Type Default
selected_key ui::TwoWayField<std::string> The selected tab's key. Empty, or a key no tab has, leaves the first tab selected. Bound two-way, the property then takes the key of the tab that is selected.
tab_width_mode ui::Binding<ui::TabsWidthMode> {ui::TabsWidthMode::Equal} How the tabs share the strip: Equal (clamped to 100..240 DIPs), SizeToContent, or Compact.
close_button_overlay_mode ui::Binding<ui::TabsCloseButtonOverlayMode> {ui::TabsCloseButtonOverlayMode::Auto} Which closable tabs show their close button.
add_tab_button_visible ui::Binding<std::optional<bool>> Shows the "+" after the last tab. Unset shows it when .on_add_tab_button_click is set.
can_reorder_tabs ui::Binding<bool> {true} Dragging a tab moves it within the strip.
strip_background ui::Binding<ui::Paint> The strip's fill: a fixed color or a theme resource; unset is transparent.
on_selection_changed ui::Callback<void(const std::string&)> Called with the new tab's key whenever the selection moves.
on_tab_close_requested ui::Callback<void(const std::string&)> Called with the tab's key on its close button, a middle click or Ctrl+F4. With a handler, nothing is removed until it calls ui::TabsControl::remove_tab(). Without one, the tab closes.
on_add_tab_button_click ui::Callback<void()> Called when the "+" is clicked; the handler adds the tab. Setting it shows the "+".
on_tabs_changed ui::Callback<void()> Called when a tab is inserted, removed or moved.
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::Tabs> Out: the handle that receives the built control.
items ui::TabList The initial tabs, in strip order.
tab_strip_header_content ui::Spec An element left of the tabs.
tab_strip_footer_content ui::Spec An element in the space right of the add button.

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

ui::Tab

One tab: a header, an optional icon, and the content shown while it is selected. Written inside the .items of a ui::Tabs, or handed to TabsControl::add_tab(). The content is a spec or an already built std::unique_ptr<Element>.

Example:

auto tabs = std::make_unique<ui::TabsControl>();
tabs->add_tab(ui::Tab{ .header = "Document 1", .key = "doc-1", .icon = ui::IconGlyph{0xE8A5},
                       .content = ui::Text{ .text = "Hello" } });
tabs->add_tab(ui::Tab{
    .header   = "Settings",
    .key      = "settings",
    .icon     = ui::IconGlyph{0xE713},
    .closable = false,
    .content  = std::make_unique<ui::TextControl>("Settings go here"),
});

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

ui::Tab{
    .header, .key, .icon, .closable, .content
}

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

Field Type Default
header ui::Binding<std::string> The tab's caption, and its tooltip. A bound header follows its property. A tab without .key keeps the key made from its first header.
key std::string What identifies the tab in .selected_key and the close and selection events. Empty takes the header, numbered " (2)", " (3)", ... after the first tab with it. It stays with the tab when the tab is dragged elsewhere or renamed.
icon ui::IconSource The icon before the header in its 16-DIP box: a Symbol, a glyph or a path in the header's color, or a picture in its own colors. Empty shows none.
closable bool true Shows the close button and accepts middle click and Ctrl+F4.
content ui::Spec What the Tabs shows while this tab is selected; owned by the Tabs once added.

ui::TabList

A built, owned list of tabs. A braced list of ui::Tab{...} converts to it, building each tab's content as it goes.

Example:

ui::TabList tabs = { ui::Tab{ .header = "One", .content = ui::Text{ .text = "1" } },
                     ui::Tab{ .header = "Two", .content = ui::Text{ .text = "2" } } };
window.set_content(ui::Tabs{ .items = std::move(tabs) });

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

ui::TabList{
    .entries
}

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

Field Type Default
entries std::vector<Entry> The tabs, in strip order.

operator=

TabList& operator=(const TabList&) = delete

operator=

TabList& operator=(TabList&&) = default

add

void add(Tab tab)

Appends one tab, building its content, for a list put together in a loop.

struct Entry

One tab with its content already built.

Field Type Default
header Binding<std::string>
node std::unique_ptr<Element>
icon IconSource
closable bool true
key std::string

ui::TabsControl

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

A strip of tabs over the selected tab's content.

The view owns every tab's content but only measures, arranges and renders the selected tab's content. Each tab has a key (Tab::key, else its header) that the events report and that stays with it when it is dragged elsewhere. The close button, a middle click and Ctrl+F4 remove the tab and select its neighbor. With an on_tab_close_requested() handler they raise that instead, and the handler decides, calling remove_tab() to close it. The "+" button shows once there is an on_add_tab_button_click() handler, unless set_add_tab_button_visible() says otherwise. Tabs that do not fit scroll, and dragging a tab reorders the strip. The declarative form is ui::Tabs.

Example:

auto  owned = std::make_unique<ui::TabsControl>();
auto* tabs  = owned.get();
tabs->add_tab("Home", std::make_unique<ui::TextControl>("Start here"));
tabs->add_tab(ui::Tab{ .header  = "Document 1",
                       .key     = "doc-1",
                       .icon    = ui::IconGlyph{0xE8A5},
                       .content = std::make_unique<ui::TextControl>("Hello") });
tabs->set_closable(0, false);
tabs->on_add_tab_button_click([tabs] {
    tabs->add_tab("New document", std::make_unique<ui::TextControl>("Empty"));
    tabs->set_selected_index(tabs->tab_count() - 1);
});
tabs->on_selection_changed([](const std::string& key) { std::println("tab {}", key); });
window.set_content(std::move(owned));

TabsControl

TabsControl()

An empty view: no tabs, and no add button until there is an add handler.

~TabsControl

~TabsControl() override

TabsControl

TabsControl(const TabsControl&) = delete

operator=

TabsControl& operator=(const TabsControl&) = delete

add_tab

template <typename T>
T* add_tab(std::string header, std::unique_ptr<T> content)

Appends a tab, taking ownership of its content, and returns a non-owning observer to the content. The first tab added becomes selected.

add_tab

Element* add_tab(ui::Tab tab)

Appends a tab, building its content; returns the content as an observer (may be null).

insert_tab

Element* insert_tab(std::size_t index, ui::Tab tab)

Inserts a tab at index (clamped), building its content; returns the content as an observer (may be null). The selection stays on the tab it was on; the first tab ever added is selected.

remove_tab

void remove_tab(std::size_t index)

Removes the tab at index and destroys its content. Removing the selected tab selects the one now at its index, or the last.

remove_tab

void remove_tab(std::string_view key)

Removes the first tab with key, as remove_tab(index_of(key)); no-op when there is none.

move_tab

void move_tab(std::size_t from, std::size_t to)

Moves a tab to another index, as dragging it does.

tab_count

std::size_t tab_count() const

The number of tabs.

const std::string& header(std::size_t index) const

The header of the tab at index; empty when out of range.

key

const std::string& key(std::size_t index) const

The key of the tab at index; empty when out of range.

index_of

std::optional<std::size_t> index_of(std::string_view key) const

The index of the first tab with key; unset when there is none.

set_header

void set_header(std::size_t index, std::string header)

Renames the tab at index (ignored out of range) and updates the strip's layout.

set_icon

void set_icon(std::size_t index, IconSource icon)

Sets or clears the icon of the tab at index and updates the strip's layout.

closable

bool closable(std::size_t index) const

Whether the tab at index can be closed.

set_closable

void set_closable(std::size_t index, bool closable)

Shows or hides the tab's close button (and its middle-click and Ctrl+F4 close).

content

Element* content(std::size_t index) const

The tab's content, or null.

selected_index

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

The selected tab; unset while there are no tabs.

set_selected_index

void set_selected_index(std::size_t index)

Selects the tab at index, clamped to the tab list. Does nothing, and fires nothing, if the tab is already selected.

selected_key

const std::string& selected_key() const

The selected tab's key; empty while there are no tabs.

set_selected_key

void set_selected_key(std::string_view key)

Selects the first tab with key. Does nothing, and fires nothing, when no tab has it or it is already selected.

on_selection_changed

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

Fired with the new tab's key whenever the selection changes: a click, Enter/Space, Ctrl+Tab, set_selected_index(), set_selected_key(), or a removal that moves it. The key is empty once the last tab is gone.

tab_width_mode

TabsWidthMode tab_width_mode() const

How the tabs share the strip; Equal by default.

set_tab_width_mode

void set_tab_width_mode(TabsWidthMode mode)

Changes the width mode and updates the layout.

close_button_overlay_mode

TabsCloseButtonOverlayMode close_button_overlay_mode() const

Which closable tabs show their close button; Auto by default.

set_close_button_overlay_mode

void set_close_button_overlay_mode(TabsCloseButtonOverlayMode mode)

Changes the overlay mode; redraws.

is_add_tab_button_visible

bool is_add_tab_button_visible() const

Whether the "+" after the last tab shows: the value set, else whether there is an on_add_tab_button_click() handler.

set_add_tab_button_visible

void set_add_tab_button_visible(std::optional<bool> visible)

Shows or hides the add button; std::nullopt (the default) shows it while there is an add handler. Updates the layout.

can_reorder_tabs

bool can_reorder_tabs() const

Whether dragging a tab moves it within the strip; true by default.

set_can_reorder_tabs

void set_can_reorder_tabs(bool can)

Allows or forbids reordering by drag.

strip_background

const Paint& strip_background() const

The strip's fill: a fixed color or a theme resource, transparent by default. The content below it is not filled.

set_strip_background

void set_strip_background(Paint color)

Sets or clears the strip's fill; redraws.

set_tab_strip_header_content

template <typename T>
T* set_tab_strip_header_content(std::unique_ptr<T> element)

The strip header, an element left of the tabs. Takes ownership and returns a non-owning observer.

template <typename T>
T* set_tab_strip_footer_content(std::unique_ptr<T> element)

The strip footer, an element in the space right of the add button; as set_tab_strip_header_content().

on_tab_close_requested

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

Fired with the tab's key when its close button, a middle click or Ctrl+F4 asks to close it. With a handler nothing is removed until it calls remove_tab(); without one the tab is removed and its neighbor selected.

on_add_tab_button_click

void on_add_tab_button_click(Callback<void()> handler)

Fired when the "+" is clicked. Adding a tab is the handler's job. Setting one shows the "+" unless set_add_tab_button_visible() set it explicitly.

on_tabs_changed

void on_tabs_changed(Callback<void()> handler)

Fired when a tab is inserted, removed or moved.

strip_bounds

Rect strip_bounds() const

The tab strip's rect.

content_bounds

Rect content_bounds() const

The selected tab's content area, below the strip.

tab_bounds

std::optional<Rect> tab_bounds(std::size_t index) const

A tab's slot in the strip (scrolled), or nullopt when it is scrolled out of view.

close_button_bounds

std::optional<Rect> close_button_bounds(std::size_t index) const

A tab's close button, or nullopt when it shows none.

scroll_decrease_bounds

std::optional<Rect> scroll_decrease_bounds() const

The scroll buttons, or nullopt while the tabs fit.

scroll_increase_bounds

std::optional<Rect> scroll_increase_bounds() const

scroll_offset

float scroll_offset() const

How far the strip is scrolled, in DIPs (0 is the first tab at the left).

add_button

ButtonControl* add_button() const

The "+" button (owned by the view).

handle_shortcut

bool handle_shortcut(const KeyEvent& e) override

Ctrl+Tab, Ctrl+Shift+Tab and Ctrl+F4 anywhere inside the view, then any shortcuts added with add_shortcut().

focused

bool focused() const

Whether the strip has keyboard focus.

focusable

bool focusable() const override

The strip is one tab stop while enabled and not empty.

takes_focus_at

bool takes_focus_at(Point p) const override

Only a press on a tab focuses the strip. The selected content takes focus on its own.

collect_focusables

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

The tabs (this), the strip header, the add button, the strip footer, then the selected tab's content.

for_each_child

void for_each_child(ChildVisitor visit) const override

Lists the tooltips, the strip header and footer, and the selected tab's content. An unselected tab's content is set aside, and nothing reaches it until it is selected again.

sync_native

void sync_native(NativeHost& host) override

Passes native-control hosting on to the strip parts and the selected tab's content.

automation_children

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

The strip header, one tab item per tab, the add button, the strip footer, then the selected tab's content. The control also exposes the selected header as a read-only value.

automation_focus

Element* automation_focus() const override

The tab the keyboard is on, so a screen reader reads it as the arrow keys move.

automation_role

AutomationRole automation_role() const override

A Tab control.

measure_override

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

Fills available on whichever axes are bounded.

arrange_override

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

Lays out the strip (tab widths, scroll buttons) and the selected tab's content below it.

render_override

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

Draws the strip and its tabs, then the selected tab's content.

pointer_override

bool pointer_override(const PointerEvent& e) override

Clicks select and close tabs, a middle click asks to close, and a drag reorders. Other pointer events reach the strip header/footer and the selected content.

wheel_override

bool wheel_override(const PointerEvent& e) override

A turn over an overflowing strip scrolls the strip. Otherwise the turn reaches the tab's content through handle_wheel(), so a scrollable inside the tab wins over one wrapping the whole Tabs.

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.

tick_override

void tick_override(Duration delta) override

Repeats a held scroll button.

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

Left/Right move the keyboard cursor between tabs; Enter/Space select it.

focus_changed

void focus_changed(bool focused) override

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

focus_ring_rect

Rect focus_ring_rect(const Theme& theme) const override

Inside the focused tab, cut to the part of it the strip shows.

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.

focus_ring_inside

bool focus_ring_inside() const override

Whether the ring goes inside focus_ring_rect() in every era, its outer edge on the rect. False by default, which leaves it to the theme. An item inside a viewport (a list row, a tab, a menu item) returns true, as does a control in a tight spot, so a neighbor or the viewport edge never cuts its ring.

Enumerations

TabsWidthMode

enum class TabsWidthMode

How a Tabs control sizes its tabs.

Value
Equal The strip divided evenly, each tab clamped to 100..240 DIPs (the default).
SizeToContent Each tab as wide as its icon, header and close button.
Compact Unselected tabs shrink to their icon.

TabsCloseButtonOverlayMode

enum class TabsCloseButtonOverlayMode

When a closable tab shows its close button.

Value
Auto the same as Always
OnPointerOver on the selected tab and the hovered one
Always on every closable tab

Tests

  • tests/test_tabs.cpp