Skip to content

Stacks

Tick UI describes a screen with stacks. Layout, clipping, identity, animation, and panel containment each have their own stack that grows as you nest into a section of the screen and shrinks back as you finish with it. Top-down code stays composable because the current state at any line is the top of every stack.

This page is a concept overview. For the procedures that work with each stack, refer to the linked guides.

What every stack has in common

Every stack follows the same contract:

  • A push gives you a smaller, narrower context to work in.
  • A pop returns to the previous one.
  • Pushes and pops must balance per frame. Tick UI asserts this in EndFrame (ValidateStacks); a leak fires a Debug.Assert.
  • The Auto() scope captures stack depth on creation and restores on dispose, so you rarely call Pop directly for layout.

The rect stack: m_rectStack

The layout stack. The most-used stack in any Tick UI screen. Every layout helper (PushRect, Pad, Center, Inset, Top, Bottom, Left, Right, and the schemas PushSeries*, PushSized, PushGrid, PushSplit, PushStack*, PushRadial) pushes onto this stack. Pop() removes one entry.

The top of the rect stack is the current rect: the area drawing calls render into. Peek() returns it. LayoutToScreen() converts to screen pixels.

Schema layouts push a container plus a current-element rect. Advance with Next() (or Next(Size) for stack mode). Terminate with Pop().

For the full set of helpers and the chainable Auto() scope, refer to Layout.

The clip stack: m_clipRectStack

The scissor stack. PushClipRect adds a clipping rectangle (in screen space, after Y-flip), and subsequent draw calls are clipped to it on the GPU. PopClip removes the top entry and restores the previous clip, or disables the scissor entirely if the stack empties.

MergeClip and MergeClipFromTop are convenience helpers that intersect the passed rect with the current top before pushing, so the new clip is always at least as tight as its parent.

Tick UI seeds this stack with a full-screen clip in BeginFrame and pops it in EndFrame. Every push you make sits on top of that seed, and your Pop returns to it.

The ID stack: m_idStack

Widget identity. Buttons, textboxes, scroll views, and panels all need a stable ID across frames so Tick UI can remember their state (focused, hovered, scroll position).

PushID(label) and PopID() namespace the IDs computed inside the scope, so the same widget code can run twice without colliding. Use this around any code that draws a list of similar items: call PushID(item.Id) per row.

In editor builds, the TICK_CHECK_DUPLICATE_IDS flag asserts when two widgets in the same frame share an ID. Define it during development and any collision shows up immediately with a callstack.

The animation stack: m_animationStack

PushAnimate("id") opens an animation context: a fluent builder that defines Init, Enter, and Leave states and per-event transitions. The opened context lives on the animation stack until PopAnimate closes it.

While the context is open, GetColor, GetFloat, GetRect, and GetInt on the returned context return the animated values for the current frame, blended according to the active state. Drawing calls read from these to get smooth hover, press, and reveal effects.

For the full DSL, refer to Animation.

The panel stack: m_panelStack

Panels are persistent containers: windows, modals, popups, the debugger overlay. BeginPanel(id, rect, config) pushes the panel onto the stack, and EndPanel pops it. Nested BeginPanel calls record parent-and-child relationships, so a child panel can be hidden when its parent hides.

Modal behaviour is a per-panel flag. Calling ShowModal(id) sets the panel's BlocksInput flag, and the input-priority walk in BeginFrame gives any blocking panel preference over non-blocking ones. A single flag drives the behaviour, with no separate modal stack to manage.

Panels also have a per-instance state machine separate from the push and pop stack: Visible, Hiding, Hidden, plus a UsedLastFrame flag so first-frame logic can run via IsPanelFirstShown.

For the full panel workflow, refer to Panels and modals.

Other stacks

A few smaller stacks support specific subsystems:

  • Color stack (m_colorStack): PushColor and PopColor tint subsequent draws. Bare Pop doesn't clear it. Use the explicit PopColor.
  • Timeline stack (m_timelineStack): BeginTimeline and EndTimeline. Used by the animation system to scope time-based state.

These follow the same push and pop balance contract.

End-of-frame validation

In EndFrame, Tick UI calls ValidateStacks() to assert that the rect, clip, panel, ID, timeline, and measure stacks are all empty (the seed clip rect having been popped by EndFrame itself). A leak means you missed a Pop, EndPanel, or PopID somewhere. The assert message names which stack leaked. Fix the imbalance before shipping; in non-development builds the asserts are stripped, so the leaked state silently corrupts the next frame.

Prefer Auto() for layout (it can't leak rect-stack entries) and guard non-Auto helpers with using scopes wherever you can.

Additional resources

  • Frame lifecycle: when stacks are seeded (BeginFrame) and validated (EndFrame).
  • Panels and modals: working with the panel and modal stacks.
  • Architecture: how the stack-using code is organised across the UI partial class.
  • Layout: the Auto() scope and the rect-stack helpers in detail.