Skip to content

Flyout

Header: winui/ui/dialogs/Flyout.hpp. Spec: winui/ui/dialogs/Flyout.hpp.

Arbitrary content floating in a popup next to an owned anchor.

The content sits on a surface with the theme's elevated menu background and a 1 DIP frame. The padding is 16, 15, 16 and 17 DIPs (left, top, right, bottom). The surface is at least 96x40 and at most 456x758 DIPs, so long text wraps at 456. Opening, placement and focus work as in FlyoutBase. A Button anchor opens it on click, and any other anchor on a click nothing inside it handled.

Example:

// Declarative:
ui::ObservableVector<std::string> cart;
ui::Control<ui::Flyout>           flyout;
window.set_content(ui::Flyout{
    .placement = ui::PopupPlacement::Right,
    .control   = flyout,
    .anchor    = ui::Button{.text = "Empty cart"},
    .content   = ui::VerticalStack{
        .spacing  = 12.0f,
        .content  = {
            ui::Text{.text = "All items will be removed. Continue?"},
            ui::Button{.text = "Yes, empty my cart", .on_click = [&] { cart.clear(); flyout->hide(); }},
        },
    },
});

// Imperative:
auto stack = std::make_unique<ui::StackControl>(ui::Orientation::Vertical);
auto owned = std::make_unique<ui::FlyoutControl>();
owned->set_anchor(std::make_unique<ui::ButtonControl>("Open"));
owned->set_content(std::make_unique<ui::TextControl>("Flyout content"));
stack->add(std::move(owned));

ui::Flyout

An anchor element with content that floats above the tree when opened. Builds a ui::FlyoutControl. A Button anchor opens it on click, Enter or Space by itself. Any other anchor opens it on a click that nothing inside it handled, by pointer only. From code, call show() on the control from .control. It opens below the anchor, and an outside click or Escape closes it.

Example:

ui::ObservableVector<std::string> cart;
ui::Control<ui::Flyout>           confirm;

window.set_content(ui::Flyout{
    .control = confirm,
    .anchor  = ui::Button{ .text = "Empty cart" },
    .content = ui::VerticalStack{
        .spacing  = 12,
        .content  = {
            ui::Text{ .text = "All items will be removed. Do you want to continue?" },
            ui::Button{ .text = "Yes, empty my cart", .on_click = [&] { cart.clear(); confirm->hide(); } },
        },
    },
});

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

ui::Flyout{
    .is_open, .placement, .placement_alignment, .show_mode, .light_dismiss, .style, .margin,
    .width, .height, .min_width, .min_height, .max_width, .max_height, .horizontal_alignment,
    .vertical_alignment, .visible, .tool_tip, .accessibility, .on_opened, .on_closing,
    .on_closed, .on_pointer_entered, .on_pointer_exited, .on_got_focus, .on_lost_focus,
    .on_key_down, .on_wheel, .control, .anchor, .content
}

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

Field Type Default
is_open ui::TwoWayField<bool> {false} Whether the flyout is showing. Two-way with ui::bind(): closing it writes false, and a false that on_closing cancels is put back to true.
placement ui::Binding<ui::PopupPlacement> {ui::PopupPlacement::Below} Which side of the anchor it opens on; PopupPlacement::Below by default.
placement_alignment ui::Binding<ui::PopupPlacementAlignment> {ui::PopupPlacementAlignment::Center} How the flyout lines up with the anchor along that side.
show_mode ui::Binding<ui::FlyoutShowMode> {ui::FlyoutShowMode::Standard} Standard takes keyboard focus while open, Transient leaves it where it is.
light_dismiss ui::Binding<bool> {true} Whether an outside click or Escape closes it; true by default. Off, it stays open until is_open turns false, hide() runs or a command in it runs.
on_opened ui::Action The flyout is showing. A two-way is_open has been written by then.
on_closing ui::Callback<ui::Decision(const ui::PopupClosingArgs&)> Gets why it is about to close; return ui::Cancel to keep it open.
on_closed ui::Callback<void(ui::PopupCloseReason)> Gets why it closed, once the close fade has finished. A two-way is_open has been written by then.
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::Flyout> Out: the handle that receives the built control.
anchor ui::Spec The in-tree element the flyout hangs from.
content ui::Spec The floating content. It is not part of the main tree. The window lays it out and draws it separately, in its popup layer.

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

ui::FlyoutControl

Derives from ui::FlyoutBase.

Arbitrary content floating in a popup next to an owned anchor.

The content sits on a surface with the theme's elevated menu background and a 1 DIP frame. The padding is 16, 15, 16 and 17 DIPs (left, top, right, bottom). The surface is at least 96x40 and at most 456x758 DIPs, so long text wraps at 456. Opening, placement and focus work as in FlyoutBase. A Button anchor opens it on click, and any other anchor on a click nothing inside it handled.

Example:

// Declarative:
ui::ObservableVector<std::string> cart;
ui::Control<ui::Flyout>           flyout;
window.set_content(ui::Flyout{
    .placement = ui::PopupPlacement::Right,
    .control   = flyout,
    .anchor    = ui::Button{.text = "Empty cart"},
    .content   = ui::VerticalStack{
        .spacing  = 12.0f,
        .content  = {
            ui::Text{.text = "All items will be removed. Continue?"},
            ui::Button{.text = "Yes, empty my cart", .on_click = [&] { cart.clear(); flyout->hide(); }},
        },
    },
});

// Imperative:
auto stack = std::make_unique<ui::StackControl>(ui::Orientation::Vertical);
auto owned = std::make_unique<ui::FlyoutControl>();
owned->set_anchor(std::make_unique<ui::ButtonControl>("Open"));
owned->set_content(std::make_unique<ui::TextControl>("Flyout content"));
stack->add(std::move(owned));

FlyoutControl

FlyoutControl() = default

A flyout with no anchor and no content.

set_content

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

Takes ownership of the floating content, replacing any previous one, and returns a non-owning observer. The content shows while is_open(). It is not part of the main tree. The window lays it out and draws it separately, in its popup layer.

content

Element* content() const

The floating content (non-owning), or nullptr.

class FlyoutBase

Derives from ui::Element.

What every flyout shares: an owned anchor element it opens from, opening and closing, placement against the anchor or at a point, focus handling, and the open and close fade.

A FlyoutBase wraps its anchor. Layout, rendering, input, focus and automation pass through to the anchor, and the flyout manages the popup. A Button anchor opens the flyout on click and with Enter or Space by itself (ButtonControl::set_flyout()). Any other anchor opens it on a left click that nothing inside the anchor handled. So a Text, an Image or a card opens it by pointer, while a CheckBox or a TextBox inside still does only its own thing. Such an anchor has no keyboard shortcut to open it, so call show() or toggle() from its own handler for that.

By default the popup opens below the anchor, centered and 4 DIPs away from it, and flips when there is no room. show_at() puts its top-left corner at a point instead, as a context menu does. FlyoutBase can't be constructed directly. Use Flyout, MenuFlyout or CommandBarFlyout.

Example:

auto  flyout = std::make_unique<ui::FlyoutControl>();
flyout->set_anchor(std::make_unique<ui::ButtonControl>("Options")); // clicks toggle the flyout
flyout->set_content(std::make_unique<ui::TextControl>("Hello"));
flyout->set_placement(ui::PopupPlacement::Above);
flyout->set_placement_alignment(ui::PopupPlacementAlignment::Leading);
flyout->on_closed([](ui::PopupCloseReason) { std::println("flyout closed"); });
window.set_content(std::move(flyout));

set_anchor

template <typename T>
T* set_anchor(std::unique_ptr<T> anchor)

Takes ownership of the anchor element, replacing any previous one, and returns a non-owning observer, as ScrollControl::set_child() does. A Button anchor toggles the flyout on click.

anchor

Element* anchor() const

The anchor element (non-owning), or nullptr.

is_open

bool is_open() const

True while the flyout is shown or fading in. It turns false as soon as hide(), an outside click or Escape commits to closing, though the popup keeps drawing while it fades out (transition_opacity()).

show

void show()

Opens the flyout against the anchor, on the side placement() names, and raises on_opened(). Does nothing while already open.

show_at

void show_at(Point point)

Opens the flyout with its top-left corner at point (window-client DIPs), as a context menu opens at the pointer.

hide

void hide()

Closes with PopupCloseReason::Programmatic. on_closing() can cancel it. Otherwise the close fade starts, is_open() turns false at once, and on_closed() runs once the fade ends. Does nothing while already closed.

toggle

void toggle()

hide() when open, show() otherwise.

placement

PopupPlacement placement() const

Which side of the anchor the content opens against; PopupPlacement::Below by default. It flips to the opposite side when there is no room.

set_placement

void set_placement(PopupPlacement placement)

Sets the side; applies the next time the popup is placed.

placement_alignment

PopupPlacementAlignment placement_alignment() const

The alignment across the placement axis; Center by default.

set_placement_alignment

void set_placement_alignment(PopupPlacementAlignment alignment)

Sets the alignment across the placement axis.

show_mode

FlyoutShowMode show_mode() const

Whether an open flyout takes keyboard focus; Standard (it does) by default.

set_show_mode

void set_show_mode(FlyoutShowMode mode)

Sets whether the next open takes keyboard focus.

light_dismiss

bool light_dismiss() const

Whether an outside click or Escape closes the flyout; true by default. Off, it stays open until hide() or a command in it closes it, and an outside click reaches what is below.

set_light_dismiss

void set_light_dismiss(bool enabled)

Turns light dismiss on or off.

on_opened

void on_opened(Callback<void()> handler)

Raised when show() or show_at() opens the flyout.

on_closing

void on_closing(Callback<Decision(const PopupClosingArgs&)> handler)

Raised with the reason before an open flyout closes; return ui::Cancel to keep it open.

on_closed

void on_closed(Callback<void(PopupCloseReason)> handler)

Raised with the reason once the flyout has closed and its close fade has finished (not when on_closing() canceled). The handler may destroy the flyout.

focusable

bool focusable() const override

Whether this element can take keyboard focus (a leaf control opts in).

handle_global_shortcut

bool handle_global_shortcut(const KeyEvent& e) override

The shortcuts that fire wherever focus is. The ones a command surface carries are of this kind (a MenuBar's Ctrl+S, a MenuFlyout item's Delete, an AppBarButton's Ctrl+N), so those controls answer here. Window offers a key that no scoped shortcut took to every element in the window, in tree order, when the focused element and its parents did not handle the key themselves (key_override()). Default: none.

raise_context_requested

bool raise_context_requested(std::optional<Point> position) override

A right-click lands on the anchor, though the flyout holds its place in the tab order. The anchor's context request handler runs first, then the flyout's own.

for_each_child

void for_each_child(ChildVisitor visit) const override

Visits the anchor, if any. The popup is not a child.

collect_focusables

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

The flyout stands in for its anchor as one tab stop. Focus itself goes to the anchor.

automation_children

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

No automation children: the anchor is not listed under the flyout.

transition_opacity

float transition_opacity() const override

A linear fade over the open and close transitions by default. A plain Flyout has no opening slide.

automation_role

AutomationRole automation_role() const override

Passes through to the anchor, like every Element override above. Screen readers see the anchor, with the open popup's content as its child.

automation_name_fallback

std::string automation_name_fallback() const override

automation_name()'s fallback when accessibility().name is empty. Default: no inherent name, correct for controls with no visible text of their own (Slider, ProgressBar), which need an explicit accessibility().name from the caller. A control with its own visible text (Button, CheckBox, TextControl, ...) overrides this to return it.

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_;
};

FlyoutBase

FlyoutBase() = default

phase

FlyoutPhase phase() const

The current phase, for a derived flyout's rendering. The popup stays open through Closing, so it fades out.

transition_progress

float transition_progress() const

How far the current Opening or Closing transition is, 0 to 1 (1 once Open or Closed).

set_transition_durations

void set_transition_durations(Duration open, Duration close)

The open and close transitions' lengths; 250 ms each by default. A derived flyout with its own motion sets them in its constructor.

positioned_at_point

bool positioned_at_point() const

Whether the current or last open was show_at(), placed at a point, instead of show().

close

void close(PopupCloseReason reason)

Closes for reason: raises on_closing(), and unless a handler cancels, starts the close fade. A derived flyout closes with PopupCloseReason::Invoked once a command in it ran.

opening_override

virtual void opening_override()

Called as show() or show_at() commits to opening, before the Opening transition starts. Reset the popup's own state here.

closing_override

virtual void closing_override()

Called as a close commits, once no on_closing() handler canceled it.

flyout_surface

virtual Element* flyout_surface() = 0

The element the flyout floats while it shows: its surface, built on first call. Null floats nothing.

virtual Element* popup_element()

The popup's element, which the flyout ticks and clears of dirty flags itself because it is not in the window's tree. flyout_surface() by default.

flyout_chrome

virtual void flyout_chrome(PopupChrome& chrome)

The shadow the window draws behind the surface; the theme's flyout shadow by default.

flyout_dismissed

virtual void flyout_dismissed()

An outside click or Escape: closes with PopupCloseReason::LightDismiss by default.

sync_popup

void sync_popup()

Opens, updates or closes the popup to match the phase, the anchor and the settings. A derived flyout calls it when its surface or chrome changes.

measure_override

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

Implement these instead of measure() and arrange(). The base class has already applied margin, size limits and alignment by the time these run. measure_override() is pure (every concrete Element must size itself); arrange_override() defaults to a no-op since most leaves need nothing beyond having bounds() set, which the base already did.

arrange_override

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

Position children within the rect arrange() settled on (also in bounds(), window DIPs). Default: nothing, for a leaf.

render_override

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

What the control draws, in layout coordinates; render() has already applied the visual layer. A container draws its children through their render(), never their render_override().

clear_dirty_override

void clear_dirty_override(DirtyFlags flags) override

Clears the anchor, and the popup too, which is outside the tree Window clears.

tick_override

void tick_override(Duration delta) override

Ticks the anchor, the popup while it shows, and the open/close transition.

pointer_override

bool pointer_override(const PointerEvent& e) override

A left click on the anchor that nothing inside it handled opens the flyout while it is closed. Presses on the open popup's content never open it again.

key_override

bool key_override(const KeyEvent& e) override

A key for this element: the focused one, or one inside it that did not handle it. Return true when the key did something here, and it goes no further; false lets it bubble on to the parent and then to the shortcuts, a dialog's default button and arrow-key focus moves. Default: false.

text_override

bool text_override(const TextInputEvent& text_event) override

Typed text for this element, routed like key_override(). Default: false.

focus_changed

void focus_changed(bool focused) override

Keyboard focus arrived (true) or left (false); is_focused() already says so. The element is redrawn either way. No-op by default.

focus_visible_changed

void focus_visible_changed(bool visible) override

focus_visible() changed; the element is redrawn either way. No-op by default.

draws_focus_ring

bool draws_focus_ring() const override

None of its own: the anchor inside it draws its own focus ring.

Enumerations

FlyoutShowMode

enum class FlyoutShowMode

Whether an open flyout takes keyboard focus.

Value
Standard takes keyboard focus while open
Transient leaves focus where it is

FlyoutPhase

enum class FlyoutPhase

Where a flyout is in its open and close cycle (FlyoutBase::phase()). During Closing, is_open() is already false, but the popup keeps drawing and fades out until the close transition ends.

Value
Closed not shown
Opening shown, fading in
Open shown
Closing committed to closing, fading out

Tests

  • tests/test_flyout.cpp