Skip to content

Dialog

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

A modal dialog over the whole window, with a title, arbitrary content and up to three buttons.

It isn't placed in the tree. Hand it to Window::show_modal(), which takes ownership and routes all input to it until it closes. Tab moves only between the dialog's controls, and focus goes back where it was afterward. The buttons are always Primary, Secondary, Close from left to right, whatever order they were set in.

The default_button() is accented and clicked by an Enter that nothing else in the dialog uses. Escape clicks the Close button, or closes with Close when there is none. Every button and Escape close the dialog. on_closing() runs first and may cancel. Then the window takes the dialog down, and on_closed() reports the result once it is gone. Window::close_modal() closes it from outside, reporting None. With no default button set, the primary button is the default when there is one. A dialog whose primary button destroys something sets Close or None instead.

The dialog is at least 320x184 and at most 548x756 DIPs, unless its width, max_width, height or max_height say otherwise. It fades in over 250 ms.

Example:

auto dialog = std::make_unique<ui::DialogControl>();
dialog->set_title("Close without saving?");
dialog->set_content(std::make_unique<ui::TextControl>("Your changes will be lost."));
dialog->set_primary_button("Close", nullptr);
dialog->set_close_button("Cancel", nullptr);
dialog->set_default_button(ui::DialogDefaultButton::Close);
dialog->on_closed([&](ui::DialogResult result) {
    if (result == ui::DialogResult::Primary)
        quit();
});
window.show_modal(std::move(dialog));

// Declaratively:
bool busy = false;
window.show_modal(ui::Dialog{
    .title          = "Close without saving?",
    .default_button = ui::DialogDefaultButton::Close,
    .primary        = {.text = "Close"},
    .close          = {.text = "Cancel"},
    .on_closing     = [&](const ui::DialogClosingArgs& args)
    { return args.result == ui::DialogResult::Primary && busy ? ui::Cancel : ui::Proceed; },
    .on_closed = [&](ui::DialogResult result)
    {
        if (result == ui::DialogResult::Primary)
            quit();
    },
    .content = ui::Text{.text = "Your changes will be lost."},
});

ui::Dialog

A modal dialog. Builds a ui::DialogControl, which a caller then hands to Window::show_modal().

Every button and Escape close the dialog by themselves. on_closing runs first and may cancel, for example while a save is still running. Then on_closed reports the result once the dialog is gone.

Example:

window.show_modal(ui::Dialog{
    .title          = "Close without saving?",
    .default_button = ui::DialogDefaultButton::Close,
    .primary        = {.text = "Close"},
    .close          = {.text = "Cancel"},
    .on_closed      = [&](ui::DialogResult result) {
        if (result == ui::DialogResult::Primary)
            quit();
    },
    .content        = ui::Text{.text = "Your changes will be lost."},
});

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

ui::Dialog{
    .title, .default_button, .primary, .secondary, .close, .style, .margin, .width, .height,
    .min_width, .min_height, .max_width, .max_height, .horizontal_alignment,
    .vertical_alignment, .visible, .tool_tip, .accessibility, .on_closing, .on_closed,
    .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
title ui::Binding<std::string> The heading above the content.
default_button ui::Binding<std::optional<ui::DialogDefaultButton>> The default button: accented, and clicked by an Enter nothing else in the dialog uses. Unset makes it the primary button when there is one, and no button otherwise.
primary ui::DialogButton Always shown Primary, Secondary, Close from left to right; an empty text leaves one out.
secondary ui::DialogButton The middle button; closes with DialogResult::Secondary.
close ui::DialogButton The right-hand button, also clicked by Escape; closes with DialogResult::Close.
on_closing ui::Callback<ui::Decision(const ui::DialogClosingArgs&)> Gets the result of every close before it happens; return ui::Cancel to keep the dialog open.
on_closed ui::Callback<void(ui::DialogResult)> Fired once the dialog is closed and gone, with what closed it.
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::Dialog> Out: the handle that receives the built control. The window owns the control once shown, until it closes.
content ui::Spec The body between the title and the buttons.

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

ui::DialogButton

One of a dialog's up-to-three buttons. Left default (empty text) means "no such button".

Example:

ui::Property<bool> can_save{false};
ui::DialogButton   save_button{ .text = "Save", .enabled = can_save, .on_click = [&] { save(); } };

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

ui::DialogButton{
    .text, .enabled, .on_click
}

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

Field Type Default
text std::string The button's caption; empty leaves the button out.
on_click ui::Action Fired on click, before the dialog starts closing.

Plus the common fields enabled every spec carries (conventions).

operator bool

operator bool() const

True when the button exists, which means it has a caption.

ui::DialogControl

Derives from ui::Element.

A modal dialog over the whole window, with a title, arbitrary content and up to three buttons.

It isn't placed in the tree. Hand it to Window::show_modal(), which takes ownership and routes all input to it until it closes. Tab moves only between the dialog's controls, and focus goes back where it was afterward. The buttons are always Primary, Secondary, Close from left to right, whatever order they were set in.

The default_button() is accented and clicked by an Enter that nothing else in the dialog uses. Escape clicks the Close button, or closes with Close when there is none. Every button and Escape close the dialog. on_closing() runs first and may cancel. Then the window takes the dialog down, and on_closed() reports the result once it is gone. Window::close_modal() closes it from outside, reporting None. With no default button set, the primary button is the default when there is one. A dialog whose primary button destroys something sets Close or None instead.

The dialog is at least 320x184 and at most 548x756 DIPs, unless its width, max_width, height or max_height say otherwise. It fades in over 250 ms.

Example:

auto dialog = std::make_unique<ui::DialogControl>();
dialog->set_title("Close without saving?");
dialog->set_content(std::make_unique<ui::TextControl>("Your changes will be lost."));
dialog->set_primary_button("Close", nullptr);
dialog->set_close_button("Cancel", nullptr);
dialog->set_default_button(ui::DialogDefaultButton::Close);
dialog->on_closed([&](ui::DialogResult result) {
    if (result == ui::DialogResult::Primary)
        quit();
});
window.show_modal(std::move(dialog));

// Declaratively:
bool busy = false;
window.show_modal(ui::Dialog{
    .title          = "Close without saving?",
    .default_button = ui::DialogDefaultButton::Close,
    .primary        = {.text = "Close"},
    .close          = {.text = "Cancel"},
    .on_closing     = [&](const ui::DialogClosingArgs& args)
    { return args.result == ui::DialogResult::Primary && busy ? ui::Cancel : ui::Proceed; },
    .on_closed = [&](ui::DialogResult result)
    {
        if (result == ui::DialogResult::Primary)
            quit();
    },
    .content = ui::Text{.text = "Your changes will be lost."},
});

DialogControl

DialogControl()

An empty dialog: no title, no content, no buttons.

set_title

void set_title(std::string title)

Sets the heading above the content (UTF-8), drawn 20 DIPs, semibold and wrapping.

title

const std::string& title() const

The title; empty by default. Screen readers also use it as the name when none is set.

set_content

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

Takes ownership of the body content and returns a non-owning observer to it, the same convention as StackControl::add(). Each further call appends below the previous content.

set_primary_button

ButtonControl* set_primary_button(std::string text, Callback<void()> on_click)

Sets the primary (leftmost) button, which closes with Primary. Each button's click runs on_click (if any), then closes the dialog with its result. Calling one again replaces that button. The visual order is fixed whatever order these are called in. Returns the button as a non-owning observer, owned by the dialog. on_click may be null.

set_secondary_button

ButtonControl* set_secondary_button(std::string text, Callback<void()> on_click)

Sets the secondary button, which closes with Secondary.

set_close_button

ButtonControl* set_close_button(std::string text, Callback<void()> on_click)

Sets the close button, also clicked by Escape, which closes with Close.

primary_button

ButtonControl* primary_button() const

The primary button, or nullptr when none was set.

secondary_button

ButtonControl* secondary_button() const

The secondary button, or nullptr when none was set.

close_button

ButtonControl* close_button() const

The close button, or nullptr when none was set.

set_default_button

void set_default_button(std::optional<DialogDefaultButton> button)

Sets the default button. It is drawn accented and clicked by an Enter that the focused element does not use itself (a Button, a list or a TextBox with a submit handler keep theirs). While focus is on one of the dialog's own buttons the accent follows focus instead. std::nullopt (the default) makes the primary button the default when there is one, and no button otherwise.

default_button

DialogDefaultButton default_button() const

The default button in effect: the one set, else Primary when there is a primary button, else None.

hide

void hide()

Close with None (on_closing still runs, and may cancel).

result

DialogResult result() const

What the last close reported; None until one happened.

on_closing

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

Raised before every close with its result; return ui::Cancel to keep the dialog open (a Primary click on a form that does not validate, say).

on_closed

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

Raised once the dialog is closed and out of the window, with what closed it. The dialog is destroyed by then, so the handler may show the next one. A dialog replaced by another show_modal() does not raise it.

for_each_child

void for_each_child(ChildVisitor visit) const override

Visits the title, the content host and the shown buttons, in visual order.

collect_focusables

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

Adds the content's focusable controls, then the shown buttons, to the dialog's tab order. The title is never a stop.

default_focus

Element* default_focus() override

Where focus starts: the first focusable thing in the content, else the default button.

transition_opacity

float transition_opacity() const override

The opening fade's opacity, rising linearly from 0 to 1 over 250 ms. The dimmed backdrop behind the dialog fades in with it. There is no closing fade. Window::close_modal() removes the dialog and restores focus at once.

automation_role

AutomationRole automation_role() const override

Always AutomationRole::Pane.

automation_name_fallback

std::string automation_name_fallback() const override

title(), used when Accessibility::name is not set.

measure_override

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

The frame, title, content and button area, clamped between 320x184 and 548x756 DIPs. A width or max width set on the dialog replaces the 548, and a height or max height the 756. When the dialog is wider than 548, the buttons keep their size at 548 and group at the right.

arrange_override

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

Places the title and content, with 24 DIPs of padding and 12 DIPs apart, and the buttons in the button area. Three buttons take a third each, two a half each, and one alone the right half.

render_override

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

Draws the frame, the title and content background, the button area and the children.

tick_override

void tick_override(Duration delta) override

Advances the open fade.

key_override

bool key_override(const KeyEvent& e) override

Enter the focused element had no use for: the default button's click, if it is enabled.

struct DialogClosingArgs

What a Dialog's on_closing gets: the result the close reports. The handler returns ui::Cancel to keep the dialog open.

Field Type Default
result DialogResult DialogResult::None Which button (or Escape) is closing it.

class DialogAwaiter

What co_await ui::show_dialog(window, spec) waits on; see show_dialog().

DialogAwaiter

DialogAwaiter(Window& window, Dialog spec)

Holds spec until the coroutine suspends.

await_ready

bool await_ready() const noexcept

Never ready: the dialog is shown when the coroutine suspends.

await_suspend

void await_suspend(std::coroutine_handle<> coroutine)

Shows the dialog and hands coroutine to its close.

await_resume

DialogResult await_resume() const noexcept

What closed the dialog.

Enumerations

DialogDefaultButton

enum class DialogDefaultButton

Which button of a dialog is the default one.

Value
None No default button: Enter clicks nothing, no button is accented.
Primary The primary (leftmost) button.
Secondary The secondary button.
Close The close (cancel) button.

DialogResult

enum class DialogResult

What closed the dialog. The Close button and Escape report Close; hide() and Window::close_modal() report None.

Value
None Closed from outside: hide() or Window::close_modal().
Primary Closed by the primary button.
Secondary Closed by the secondary button.
Close Closed by the Close button or Escape.

Functions

show_dialog

DialogAwaiter show_dialog(Window& window, Dialog spec)

Shows spec modally over window and lets a coroutine wait for the answer. co_await ui::show_dialog(window, spec) gives the DialogResult the dialog closed with, so a flow of questions reads top to bottom without nested on_closed handlers. The spec's own on_closing, which may still cancel, and on_closed still run. on_closed runs before the coroutine goes on.

The coroutine resumes on the UI thread inside the window's next tick(), as after on_ui_thread(). When the dialog goes without an answer, the coroutine is dropped instead and its frame destroyed without resuming. That happens when another modal replaces the dialog, when the Window is destroyed with it open, or when the window is closed by the time the answer would resume it. A coroutine that has to clean up does it in a destructor, as with on_ui_thread(). Call it on the UI thread, from a coroutine such as a ui::FireAndForget.

With MSVC, compute texts that come from a function call into local variables before the co_await. MSVC can destroy a temporary of a co_await expression too early, and the call that reads it crashes.

Example:

ui::FireAndForget delete_files(ui::Window& window, ui::Property<std::string>& status)
{
    const ui::DialogResult answer = co_await ui::show_dialog(window, ui::Dialog{
        .title          = "Delete 3 files?",
        .default_button = ui::DialogDefaultButton::Close,
        .primary        = {.text = "Delete"},
        .close          = {.text = "Cancel"},
        .content        = ui::Text{.text = "They go to the recycle bin."},
    });
    if (answer != ui::DialogResult::Primary)
        co_return;
    status = "Deleted";
}

Tests

  • tests/test_dialog.cpp