8.19.11. With id

ImGui hashes a widget’s identifier into the ID stack to detect clicks, remember imgui.ini state, and tag draw-list entries. The boost layer piggybacks on the same hash for its telemetry path. When two render sites need to share a widget identifier — composed helpers, two panes showing the same kind of control, a button rendered under two scopes — the boost ships three knobs for sorting out the collision:

  • with_id("scope") { ... } pushes "scope" onto both ImGui’s ID stack and the boost path, so two scopes contain the same widget identifier without collision.

  • widget(IDENT, (id = "x")) mangles only ImGui’s hash — useful when the daslang identifier may get renamed but the imgui.ini state and any hex_id-driven scripts must stay stable.

  • widget(IDENT, (path = "x")) replaces the registry path leaf — the daslang identifier still names the global; "x" is what external drivers target via imgui_force_set / imgui_click.

Source: modules/dasImgui/examples/tutorial/with_id.das.

8.19.11.1. Walkthrough

  1options gen2
  2options _comment_hygiene = true
  3options gc
  4
  5require imgui
  6require imgui_app
  7require opengl/opengl_boost
  8require live/glfw_live
  9require live/live_api
 10require live/live_commands
 11require live/live_vars
 12require live_host
 13require imgui/imgui_live
 14require imgui/imgui_boost_runtime
 15require imgui/imgui_boost_v2
 16require imgui/imgui_widgets_builtin
 17require imgui/imgui_containers_builtin
 18require imgui/imgui_id_builtin
 19require imgui/imgui_visual_aids
 20
 21// =============================================================================
 22// TUTORIAL: with_id — scope IDs and registry paths for composed render sites.
 23//
 24// Each widget's ImGui hash is derived from its identifier — the same global
 25// rendered at two sites would hash to the same value and collide. The boost
 26// already enforces "single-global rendered exactly once per frame" at the
 27// registry level (loops want the indexed form `IDENT[i]`), but real apps
 28// still need to compose helpers that surface the same widget kind under
 29// distinct labels. Three knobs cover the cases:
 30//
 31//   with_id("scope") { ... }         — push "scope" onto BOTH the ImGui ID
 32//                                      stack AND the boost path. Distinct
 33//                                      scopes → distinct hex_ids AND distinct
 34//                                      registry paths.
 35//   widget(IDENT, (id = "x"))        — mangle ImGui's hash with "x" only;
 36//                                      telemetry path stays the bare
 37//                                      identifier (script-stable across
 38//                                      label renames; preserves imgui.ini).
 39//   widget(IDENT, (path = "x"))      — replace the telemetry path leaf with
 40//                                      "x"; the bare identifier still names
 41//                                      the daslang global. Mangles BOTH the
 42//                                      path AND the ImGui hash.
 43//
 44// STANDALONE: daslang.exe modules/dasImgui/examples/tutorial/with_id.das
 45// LIVE:       daslang-live modules/dasImgui/examples/tutorial/with_id.das
 46//
 47// DRIVE (when running live):
 48//   curl -X POST -d '{"name":"imgui_snapshot"}'                                                       localhost:9090/command
 49//   curl -X POST -d '{"name":"imgui_click","args":{"target":"ID_WIN/section_a/SAVE_BTN"}}'             localhost:9090/command
 50//   curl -X POST -d '{"name":"imgui_click","args":{"target":"ID_WIN/section_b/SAVE_BTN"}}'             localhost:9090/command
 51//   curl -X POST -d '{"name":"imgui_force_set","args":{"target":"ID_WIN/rps_stable","value":0.7}}'           localhost:9090/command
 52// =============================================================================
 53
 54[export]
 55def init() {
 56    live_create_window("dasImgui with_id tutorial", 720, 520)
 57    live_imgui_init(live_window)
 58    let io & = unsafe(GetIO())
 59    GetStyle().FontScaleMain = 1.5
 60}
 61
 62[export]
 63def update() {
 64    if (!live_begin_frame()) return
 65    begin_frame()
 66
 67    ImGui_ImplGlfw_NewFrame()
 68    apply_synth_io_override()
 69    NewFrame()
 70
 71    SetNextWindowPos(ImVec2(30.0f, 30.0f), ImGuiCond.FirstUseEver)
 72    SetNextWindowSize(ImVec2(640.0f, 440.0f), ImGuiCond.FirstUseEver)
 73    window(ID_WIN, (text = "with_id", closable = false,
 74                    flags = ImGuiWindowFlags.None)) {
 75
 76        // ---- with_id scope: same widget, two render sites ----
 77
 78        // SAVE_BTN is a single-global widget; rendering it twice without with_id
 79        // panics at end-of-frame (registry N>1). Each scope adds a path segment +
 80        // PushID, so ID_WIN/section_a/SAVE_BTN and .../section_b/... stay distinct.
 81        text("Same SAVE_BTN, two scopes:")
 82        with_id("section_a") {
 83            button(SAVE_BTN, (text = "Save in A"))
 84        }
 85        with_id("section_b") {
 86            button(SAVE_BTN, (text = "Save in B"))
 87        }
 88        text("SAVE_BTN.click_count aggregates: {SAVE_BTN.click_count}")
 89
 90        separator(WI_SEP_1)
 91
 92        // ---- Nested chains: outer scope, inner scope, leaf widget ----
 93        // The path is the chain joined by "/": ID_WIN/outer/inner/NESTED_BTN.
 94        text("Nested with_id chains:")
 95        with_id("outer") {
 96            with_id("inner") {
 97                button(NESTED_BTN, (text = "Nested deep"))
 98            }
 99        }
100
101        separator(WI_SEP_2)
102
103        // ---- id= per-call: stable ImGui hash across label renames ----
104
105        // HASHED_BTN (the daslang identifier) drives visibility + live-reload; the
106        // imgui hash is mangled by "stable_v1", so hex_id-targeting scripts and
107        // imgui.ini state survive a rename. Telemetry path stays ID_WIN/HASHED_BTN.
108        text("Per-call id= - script-stable ImGui hash:")
109        button(HASHED_BTN, (text = "Hashed via id=", id = "stable_v1"))
110
111        separator(WI_SEP_3)
112
113        // ---- path= per-call: stable telemetry path across renames ----
114
115        // RPS_PATH is the daslang global (rename breaks the reference). "rps_stable"
116        // is the registry path leaf — the imgui_force_set / imgui_click string for
117        // external drivers; both the path AND the ImGui hash adopt "rps_stable".
118        text("Per-call path= - script-stable telemetry path:")
119        slider_float(RPS_PATH, (text = "Speed (path=)",
120                                path = "rps_stable"))
121    }
122
123    end_of_frame()
124    Render()
125    var w, h : int
126    live_get_framebuffer_size(w, h)
127    glViewport(0, 0, w, h)
128    glClearColor(0.10f, 0.10f, 0.12f, 1.0f)
129    glClear(GL_COLOR_BUFFER_BIT)
130    live_imgui_render()
131
132    live_end_frame()
133}
134
135[export]
136def shutdown() {
137    live_imgui_shutdown()
138    live_destroy_window()
139}
140
141[export]
142def main() {
143    init()
144    while (!exit_requested()) {
145        update()
146        maybe_collect_gc()
147    }
148    shutdown()
149}

8.19.11.1.1. Requires

One extra module on top of the baseline boost layer:

  • imgui/imgui_id_builtin — defines with_id(s) { ... } and wires the id= / path= named-arg sugar for every widget macro.

8.19.11.1.2. The single-render rule

Single-global widgets render exactly once per frame. Two violations collapse into one runtime panic at end-of-frame:

  • Single-global widget inside a for loop (would render N times) — the boost macro can usually catch this lexically and macro_error at expansion with a fixit pointing at the indexed form IDENT[i] / IDENT[key] (see Widgets tour for the indexed form).

  • Single-global widget called from two distinct sites (e.g. shown in two windows at once) — when the macro can’t see the second site lexically, the runtime registry catches it and panics.

with_id is the explicit ID-stack push for case 2 when the indexed form can’t reach (composed helpers, sub-tree data-driven structure).

8.19.11.1.3. with_id scopes

Each with_id("scope") { ... } block pushes "scope" onto two stacks: ImGui’s ID stack (so child widgets get distinct hashes) and the boost registry path (so child entries register under distinct paths). Both pop on block exit:

with_id("section_a") {
    button(SAVE_BTN, (text = "Save in A"))
}
with_id("section_b") {
    button(SAVE_BTN, (text = "Save in B"))
}

The two renders show up in the snapshot as ID_WIN/section_a/SAVE_BTN and ID_WIN/section_b/SAVE_BTN — distinct paths AND distinct ImGui hashes. SAVE_BTN.click_count is one global; clicks on either button increment the same counter. If you need separate counters, switch to SAVE_BTN[key] indexed-widget form instead.

8.19.11.1.4. Nested chains

with_id chains nest. Each scope contributes one path segment, and the leaf widget’s registry path is the chain joined by /:

with_id("outer") {
    with_id("inner") {
        button(NESTED_BTN, (text = "Nested deep"))
    }
}
// registry path: ID_WIN/outer/inner/NESTED_BTN

with_id doesn’t register a container entry of its own — only the path segment. The leaf widget’s entry is what shows up in the snapshot.

8.19.11.1.5. Per-call id= — stable hash across renames

The daslang identifier is the source-of-truth name for a widget — it defines the global, drives live-reload visibility, and determines how the variable is referenced elsewhere in the program. The ImGui hash and the registry path are conventions built on top.

id="x" decouples the ImGui hash from the identifier:

button(HASHED_BTN, (text = "Hashed via id=", id = "stable_v1"))

Telemetry path is still ID_WIN/HASHED_BTN (the bare identifier); ImGui’s hash is mangled by "stable_v1" via PushID/PopID wrapping the render call. Practical wins:

  • imgui.ini stability — rename HASHED_BTN to SAVE_BUTTON_V2 in source, and imgui.ini’s open/close state for this button still matches the same ImGui hash; users don’t lose their layout.

  • Hex_id-driven scripts — external drivers that target widgets by the hex_id field in the snapshot keep working across daslang identifier renames.

8.19.11.1.6. Per-call path= — stable registry path across renames

path="x" is the inverse case — keep the daslang identifier internal, expose a stable string to external drivers:

slider_float(RPS_PATH, (text = "Speed (path=)", path = "rps_stable"))

The registry path becomes ID_WIN/rps_stable (the bare identifier is NOT registered). RPS_PATH is still the daslang global — rename it without breaking script drivers. path= also mangles the ImGui hash since widget_prelude does PushID(widget_ident) and widget_ident is the overridden value.

8.19.11.1.7. Standalone vs live

Same convention as previous tutorials: daslang for standalone or daslang-live to keep the live-reload server running.

8.19.11.1.8. Driving from outside

The registry paths shown above are exactly what external drivers target:

curl -X POST -d '{"name":"imgui_click","args":{"target":"ID_WIN/section_a/SAVE_BTN"}}' \
     localhost:9090/command
curl -X POST -d '{"name":"imgui_click","args":{"target":"ID_WIN/section_b/SAVE_BTN"}}' \
     localhost:9090/command
curl -X POST -d '{"name":"imgui_force_set","args":{"target":"ID_WIN/rps_stable","value":0.7}}' \
     localhost:9090/command

Note the third targets rps_stable (the path= override), not the RPS_PATH identifier — the override replaces the bare-identifier path entry, so the bare-name target would return “no such widget.”

8.19.11.1.9. Next steps

State and telemetry come next — the registered widget state structs that back every boost widget, how dotted flags on the identifier (RPS.PUBLIC.NOTLIVE) tune cross-module visibility and live-reload behavior, and the auto-emit hook that surfaces app-side values to the snapshot.

See also

Full source: modules/dasImgui/examples/tutorial/with_id.das

Richer reference: modules/dasImgui/examples/features/id_override.das — the features-side demo with the same surface plus a same-state shared-button demonstration.

Integration test: modules/dasImgui/tests/test_id_override.das.

Previous tutorial: With style

Boost macros — the macro layer.