Skip to content

IDs

Tick UI keeps state for buttons, textboxes, scroll views, and panels keyed by a widget ID. The ID is derived from the strings you pass to Button("save"), BeginPanel("settings", …), and other widget calls. This page explains how IDs are computed, why duplicates are bugs, and how PushID and PopID keep repeated widgets distinct.

Why widget IDs exist

Persistent UI state (focus, hover, scroll position, panel visibility) needs a stable key across frames. The widget call site does not know its own object identity in an immediate-mode model, because the call exists only for the duration of DoUI. You supply the identity as a string.

Two widgets that produce the same combined ID share state. That is the source of most ID-related bugs.

For the broader question of where state lives in Tick UI, refer to State.

How IDs are computed

A widget's ID is the string.GetHashCode() of the string passed to the widget call. The hash is then combined with anything on the ID stack via PushID, so the same widget code inside two different scopes produces two different IDs.

The combined ID is an int. Collisions are rare in practice, and the editor-only TICK_CHECK_DUPLICATE_IDS flag logs an error from PushID when a collision happens within a single frame.

The ID stack

m_idStack accumulates int hashes pushed via PushID(label). While the stack is non-empty, every widget call combines its own hash with the stack contents to produce its final ID.

A Button("ok") call inside PushID("dialog_a") and a Button("ok") call inside PushID("dialog_b") produce different combined IDs, so they keep separate state.

For the per-stack contract, refer to Stacks.

When to use PushID and PopID

Wrap any code that draws repeated widgets in PushID and PopID. The classic cases are:

  • A row per item in a list: PushID(item.Id) per iteration.
  • A button per inventory slot: PushID(slot.Id) per slot.
  • A reusable widget called from multiple places: each call site pushes a unique ID before invoking the widget.

Without PushID, every iteration produces the same combined ID and only one row responds correctly to clicks.

For the per-widget recipe with code, refer to Make a button and State.

Duplicate-ID detection in editor builds

Define TICK_CHECK_DUPLICATE_IDS in editor builds to catch ID collisions early. PushID then logs an error when two pushes in the same frame produce the same combined ID, and the message includes the earlier call site's callstack. BeginFrame resets the per-frame tracking each frame.

For the full define list, refer to Compile flags.

What state lives behind an ID

Tick UI owns a per-ID state record for each widget that needs one:

Widget State keyed by ID
Button Hover and press flags, last-frame-clicked flag.
Textbox Focus, caret position, selection range.
ScrollView Scroll position.
BeginPanel Visibility (Visible / Hiding / Hidden), UsedLastFrame, parent index.
PushAnimate Animation state machine and timing.

For the panel state machine, refer to Panels and modals. For animation contexts, refer to Animation.

Pitfalls

Forgetting PushID in a loop. : Every iteration shares one combined ID. Only one widget behaves correctly. Pair PushID(item.Id) and PopID() around each iteration.

Two unrelated widgets share a string by accident. : Usually fine, because the surrounding panel ID differentiates them. Becomes a problem if you flatten panels or skip the BeginPanel boundary.

A widget's state resets every frame. : The string passed to the widget changed between frames. Keep IDs stable across frames; do not embed frame-counter values in them.

The duplicate-ID error never fires. : TICK_CHECK_DUPLICATE_IDS is not defined for the active Editor platform. Add it under Project Settings > Player > Other Settings > Scripting Define Symbols.

Additional resources