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¶
- State: the broader picture of where state lives.
- Stacks: the ID stack contract.
- Make a button: the canonical
PushID-in-a-loop pattern. - Compile flags:
TICK_CHECK_DUPLICATE_IDS.