Skip to content

Layers and Modals

Ratcn paints in declaration order: what you declare later paints on top of what you declared earlier. Two mechanisms go beyond that order — deferred paint for passive overlays, and modal layers for dialogs. Pick the smallest one that does the job:

MechanismPaint time and purposeInteraction
Direct paint (render_widget)Immediate; ordinary Ratatui decoration or paint-only widgetsNone
render_componentImmediate component paint in declaration orderIdentity, geometry, focus, hover, events
defer_paintAfter the ordinary declarations in the current layerPassive paint only
modalDims everything declared so far and opens a new layer on topNew active event layer

Use defer_paint for passive overlays that must land on top of the current layer — a floating dragged card, tooltip-like decoration. The closure receives a Painter over the frame plus the app state; it has no identity and no hit target, so it cannot receive events, and it is not a way to defer an interactive component. Direct paint in the right declaration position is simpler when ordering already works out.

A modal is stronger: it becomes the active layer. While it is open, keyboard and mouse routing are confined to it, and input that nothing in the modal handles is absorbed rather than reaching the UI underneath. Declare all base content before the first modal, and declare stacked modals bottom to top.

Semantic Modal State

Whether a modal is open is app state, like everything else. ModalState stores the stack of open modal IDs plus, for each, the focus to restore when it closes. Open and close it in update, and declare the modal whenever your state says it is open:

rust
state.modals.open("confirm", &mut state.focus)?;

ratcn.render(frame, &state, &state.theme, |ctx| {
    // Base paint and declarations first.
    if state.modals.is_open("confirm") {
        let area = ctx.area();
        ctx.modal("confirm", dialog, area);
    }
});

state.modals.close(&mut state.focus);

open saves the current focus and moves focus intent to the new modal. close pops the top modal and restores its exact saved focus. Ratcn provides the stack and the focus bookkeeping; when and which modal opens stays your decision.

The modal root does not have to be a component. RenderCtx::modal_scope opens the same layer around a plain scope closure — paint your own chrome and declare children with render_component, exactly like a base-layer panel. Reach for it to hand-roll a dialog-like layer that stays entirely app-owned; Dialog is the packaged alternative with chrome, dragging, and dismiss keys built in.

Binding the Stack

Tell the runtime where your modal stack lives:

rust
let ratcn = Ratcn::new()
    .modals(|state: &AppState| &state.modals);

With the binding in place, every render must declare exactly the modal IDs the state says are open — a mismatch is a declaration bug and fails the render. The binding also lets the runtime know which modal is topmost before the frame is declared, keeping focus painting and event routing aligned, and it covers the brief gap between opening or closing a modal in update and the redraw that reflects it: events arriving in that gap are consumed instead of landing on a layer your state considers closed.

See Dialog for the packaged modal component.