Skip to content

Dialog

Dialog is a centered, bordered box with a title, a body, and an action row. It is an ordinary composite component — what makes it modal is declaring it with ctx.modal(...), which puts it on its own layer above everything else.

Default

A title, a description, and two actions. Dialog measures and places the action row itself, so there is no layout code for it.

rust
use ratcn::{Button, Dialog};

Dialog::new()
    .title("Delete item")
    .description("This cannot be undone.")
    .on_dismiss(|| Msg::Cancel)
    .action("cancel", Button::new("Cancel").secondary().on_press(|| Msg::Cancel))
    .action("delete", Button::new("Delete").destructive().on_press(|| Msg::Delete))

Tab moves between actions and wraps inside the dialog, Enter presses the focused one, and Escape emits .on_dismiss(...). A different key or chord can take Escape's place via .dismiss_key(...) — it accepts a char, a KeyCode, or a KeyChord such as KeyChord::from('w').ctrl(). Drag the border to move the box. The box uses theme.surface; the modal backdrop further separates it by dimming the rest of the app.

Wrapping is what a dialog wants, so it is the default. .tab_wrap(TabWrap::Escape) lets traversal leave the dialog's scope when it is used as an ordinary component. When the dialog is declared with ctx.modal(...), the modal boundary still contains Tab traversal; it never reaches the base layer.

Opening And Closing

The app decides when a dialog opens. Keep a ModalState beside focus and bind it, then declare the dialog when that state says it is open:

rust
use ratcn::runtime::{ModalState, Ratcn};

let ratcn = Ratcn::new()
    .focus(|s: &AppState| &s.focus, Msg::FocusChanged)
    .modals(|s: &AppState| &s.modals);

// In update():
state.modals.open("confirm", &mut state.focus)?;
state.modals.close(&mut state.focus);

// In render(), after the base layer:
ratcn.render(frame, &state, &theme, |ctx| {
    // ... base content first ...
    if state.modals.is_open("confirm") {
        ctx.modal("confirm", dialog, ctx.area());
    }
});

open saves the focus the user had; close puts it back exactly. Binding .modals(...) is what stops a keypress landing on a dialog the app already considers closed, and keeps focus correct on the dialog's first frame.

See Layers and Modals for declaration ordering and the full layering contract.

Custom Content

Use .description(...) for confirmations. For anything else, .content(...) gives you the body area and the normal declaration API:

rust
Dialog::new()
    .content(move |ctx| {
        ctx.render_component("options", List::new(options), ctx.area());
    })

Focusable children just work — the runtime discovers them during the frame's structure pass. Children live in the dialog's sibling namespace, so their ids must not collide with action ids.

.footer(...) does the same for the action row when it needs custom layout — a checkbox on the left, a status message beside the buttons. It is one row tall unless .footer_height(...) says otherwise, and cannot be combined with .action(...).

Sizing

The box sizes itself to its description. Set .width(...) or .height(...) to fix either dimension; both are clamped to the area the dialog is given. The dialog can measure a description but not a custom closure, so .content(...) requires .content_height(...) or an explicit .height(...) — declaring content without either panics.

Dragging

Pass the app-owned offset with .offset(...) and add .on_offset_change(...) to make the border draggable. Without the handler the dialog does not move. The emitted offset is already clamped to keep the box on screen. See Dragging.

Full API

Every method, with panics and edge-case detail: Dialog, ModalState.