Skip to content

Panels and modals

Tick UI groups draw work into named, persistent containers called panels. A window, a modal, a popup, and the built-in debugger overlay are all panels. They differ in their PanelConfig and in how you show them. This page covers the panel lifecycle, the visibility states, and how Tick UI resolves which panel owns input when several overlap.

What a panel is

A panel is identified by a string name (hashed via string.GetHashCode()), so the same BeginPanel("settings", …) call across frames refers to the same panel. Panels persist between frames. Tick UI remembers their visibility, draw command list, and whether they were used last frame.

if (ui.BeginPanel("settings", panelRect, PanelConfig.Default))
{
    // panel body. Only runs while the panel is visible.
    ui.EndPanel();
}

BeginPanel returns false when the panel is hidden, so always guard the body with the return value. EndPanel only runs when BeginPanel returned true.

The visibility state machine

Each panel has one of three states:

  • Visible: the panel renders this frame and processes input.
  • Hiding: the closing frame. BeginPanel returns true for one more frame so the body runs a final time, but the panel no longer accepts input. EndPanel then moves it to Hidden.
  • Hidden: the panel is not active. BeginPanel returns false.

Toggle states with ShowPanel("id") and HidePanel("id"). Place these calls outside the matching BeginPanel block. They are declarative state changes that take effect from the next frame.

if (ui.Button("open_settings"))
    ui.ShowPanel("settings");

To start a panel hidden, set Hidden = true on the PanelConfig passed to BeginPanel. The first time ShowPanel (or ShowModal) is called, the panel transitions to Visible and renders.

First-frame initialisation

A panel can seed state the first time it appears, such as focusing a textbox or resetting a scroll position. Detect this with IsPanelFirstShown():

if (ui.BeginPanel("login", rect, PanelConfig.Default))
{
    if (ui.IsPanelFirstShown())
        ui.SetFocus("username");

    ui.EndPanel();
}

IsPanelFirstShown() returns true for the frame the panel was not used last frame. This covers the first appearance and any time the panel was hidden then re-shown.

Configuration with PanelConfig

BeginPanel takes a PanelConfig:

Field Effect
Hidden Start hidden; require an explicit ShowPanel / ShowModal.
AcceptsInput When false, the panel never owns input even when it overlaps the mouse. Use for read-only HUDs.
AutoCloseOnOutsideClick The panel hides itself when the user clicks outside its rect. The behaviour that powers popups.
BackdropColor When set, Tick UI fills the screen with this colour before the panel body runs. Use for the dim "behind a modal" effect.

Three named presets cover the common shapes:

  • PanelConfig.Default: visible, accepts input, no backdrop. The baseline window.
  • PanelConfig.ModalDefault: Default plus a semi-transparent black BackdropColor. Pair with ShowModal for the canonical modal.
  • PanelConfig.OverlayDefault: visible, does not accept input. The HUD overlay shape that draws but never intercepts clicks.

The show call you use, not the config, controls whether a panel blocks input to anything underneath:

  • ShowPanel(id): visible, does not block.
  • ShowModal(id): visible, sets BlocksInput. The input-priority walk in BeginFrame prefers any blocking panel over non-blocking ones, so nothing below the modal receives input.

Modals

A modal is a panel with a BackdropColor shown via ShowModal. The backdrop draws the dim layer behind it automatically. ShowModal makes it block input below.

The standard pattern:

if (ui.BeginPanel("confirm_quit", rect, new PanelConfig
{
    Hidden = true,
    BackdropColor = new Color(0, 0, 0, 0.5f),
}))
{
    using (ui.Auto().Center(360, 160))
    {
        ui.FillRounded(panelColor, 8);
        ui.Text("Quit without saving?", font, 18, Color.white,
            TextAlign.MiddleCenter);

        // ... buttons ...
    }

    ui.EndPanel();
}

if (userPressedQuit)
    ui.ShowModal("confirm_quit");

Note the following:

  • BackdropColor draws the dim layer. The body needs no separate ui.Fill and no ConsumeInput, because the modal already blocks input below via ShowModal.
  • Hidden = true plus ShowModal(id) starts the panel hidden and opens it as a modal when triggered. To keep the panel always visible and modal whenever it is open, omit Hidden and call ShowModal at first appearance.
  • The body rect (screenRect above) usually covers the whole screen so the centred dialog can position itself anywhere inside it. The panel's own Rect defines its hit-test area. The backdrop draws independently across the whole root rect.

Popups

Popups are panels that close themselves when the user clicks outside their rect. Use them for context menus, dropdowns, and ephemeral choosers. There is a dedicated BeginPopup / EndPopup API that wraps a PanelConfig with AutoCloseOnOutsideClick = true:

if (ui.BeginPopup("dropdown", rect, hidden: true))
{
    // popup body
    ui.EndPopup();
}

if (userClickedTrigger)
    ui.ShowPopup("dropdown", mousePosition);

Popups dismiss on outside interaction and leave input to other panels intact. To build a popup that also blocks input underneath, use BeginPanel directly with new PanelConfig { AutoCloseOnOutsideClick = true, BackdropColor = ... }, then show via ShowModal.

How Tick UI resolves the hovered panel

When several panels overlap on screen, one of them owns the input for that frame. Tick UI resolves this in BeginFrame:

  1. Tick UI sorts blocking panels (those shown via ShowModal) to the end of the panel list.
  2. The frame walks the panel list once and remembers the last panel that matched, so the topmost match wins.
  3. A panel matches if BlocksInput is true, or if AcceptsInput is true and its rect contains the mouse.

The selected panel becomes the hovered panel. Only widgets inside it receive hover, click, and keyboard events for that frame. A panel that is not the hovered panel still renders, but its Button and Textbox widgets do not respond.

The built-in debugger overlay (TICK_DEBUGGER) stays on top of your UI through a separate mechanism: its panels live in the Debugger scene rather than the User scene, and EndFrame draws the debugger scene in its own layer after every user panel.

Exclude a panel from input

A panel with AcceptsInput = false (the shape that PanelConfig.OverlayDefault provides) draws normally but never matches in the hovered-panel walk, regardless of mouse position. Use it for read-only HUDs that draw on top of the game and do not intercept clicks.

Consume input inside a panel body

ui.ConsumeInput() claims input from inside a panel body. With ShowModal already blocking everything underneath, the dim-background scenario rarely needs it. Use it for a non-blocking panel that swallows clicks in a sub-rect, such as a transparent overlay where clicks within a button must not fall through to the scene beneath.

Additional resources

  • Stacks. The panel stack in context.
  • Frame lifecycle. Where input-focus resolution sits in the per-frame flow.
  • Show a modal. The procedure for the modal pattern described above.
  • Animation guide. Combining panel show and hide with animations.