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 viaimgui_set/imgui_click.
Source: examples/tutorial/with_id.das.
Walkthrough
1options gen2
2
3require imgui
4require imgui_app
5require glfw/glfw_boost
6require opengl/opengl_boost
7require live/glfw_live
8require live/live_api
9require live/live_commands
10require live/live_vars
11require live/opengl_live
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_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 var io & = unsafe(GetIO())
59 io.FontGlobalScale = 1.5
60}
61
62[export]
63def update() {
64 if (!live_begin_frame()) return
65 begin_frame()
66
67 ImGui_ImplOpenGL3_NewFrame()
68 ImGui_ImplGlfw_NewFrame()
69 apply_synth_io_override()
70 NewFrame()
71
72 SetNextWindowPos(ImVec2(30.0f, 30.0f), ImGuiCond.FirstUseEver)
73 SetNextWindowSize(ImVec2(640.0f, 440.0f), ImGuiCond.FirstUseEver)
74 window(ID_WIN, (text = "with_id", closable = false,
75 flags = ImGuiWindowFlags.None)) {
76
77 // ---- with_id scope: same widget, two render sites ----
78 // SAVE_BTN is a single-global widget. Without with_id, rendering it
79 // twice would panic at end-of-frame (registry detects N>1). Each
80 // scope contributes a path segment AND a PushID, so the registry
81 // sees ID_WIN/section_a/SAVE_BTN and ID_WIN/section_b/SAVE_BTN as
82 // distinct entries with distinct ImGui hashes.
83 text("Same SAVE_BTN, two scopes:")
84 with_id("section_a") {
85 button(SAVE_BTN, (text = "Save in A"))
86 }
87 with_id("section_b") {
88 button(SAVE_BTN, (text = "Save in B"))
89 }
90 text("SAVE_BTN.click_count aggregates: {SAVE_BTN.click_count}")
91
92 separator(WI_SEP_1)
93
94 // ---- Nested chains: outer scope, inner scope, leaf widget ----
95 // The path is the chain joined by "/": ID_WIN/outer/inner/NESTED_BTN.
96 text("Nested with_id chains:")
97 with_id("outer") {
98 with_id("inner") {
99 button(NESTED_BTN, (text = "Nested deep"))
100 }
101 }
102
103 separator(WI_SEP_2)
104
105 // ---- id= per-call: stable ImGui hash across label renames ----
106 // The button's daslang identifier (HASHED_BTN) controls visibility
107 // and live-reload; the imgui hash is mangled by "stable_v1" so
108 // scripts targeting the widget by hex_id, and imgui.ini state for
109 // this button, stay stable even if HASHED_BTN is later renamed.
110 // Telemetry path stays ID_WIN/HASHED_BTN.
111 text("Per-call id= — script-stable ImGui hash:")
112 button(HASHED_BTN, (text = "Hashed via id=", id = "stable_v1"))
113
114 separator(WI_SEP_3)
115
116 // ---- path= per-call: stable telemetry path across renames ----
117 // RPS_PATH is the daslang global (rename it and the variable
118 // reference breaks). "rps_stable" is the registry path leaf — the
119 // imgui_set / imgui_click string used by external drivers. Both the
120 // path AND the ImGui hash adopt "rps_stable".
121 text("Per-call path= — script-stable telemetry path:")
122 slider_float(RPS_PATH, (text = "Speed (path=)",
123 path = "rps_stable"))
124 }
125
126 end_of_frame()
127 Render()
128 var w, h : int
129 live_get_framebuffer_size(w, h)
130 glViewport(0, 0, w, h)
131 glClearColor(0.10f, 0.10f, 0.12f, 1.0f)
132 glClear(GL_COLOR_BUFFER_BIT)
133 ImGui_ImplOpenGL3_RenderDrawData(GetDrawData())
134
135 live_end_frame()
136}
137
138[export]
139def shutdown() {
140 live_imgui_shutdown()
141 live_destroy_window()
142}
143
144[export]
145def main() {
146 init()
147 while (!exit_requested()) {
148 update()
149 }
150 shutdown()
151}
Requires
One extra module on top of the baseline boost layer:
imgui/imgui_id_builtin— defineswith_id(s) { ... }and wires theid=/path=named-arg sugar for every widget macro.
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
forloop (would render N times) — the boost macro can usually catch this lexically andmacro_errorat expansion with a fixit pointing at the indexed formIDENT[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).
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.
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.
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_BTNtoSAVE_BUTTON_V2in 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_idfield in the snapshot keep working across daslang identifier renames.
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.
Standalone vs live
Same convention as previous tutorials: daslang for standalone or
daslang-live to keep the live-reload server running.
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_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.”
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: examples/tutorial/with_id.das
Richer reference: examples/features/id_override.das — the
features-side demo with the same surface plus a same-state shared-button
demonstration.
Integration test: tests/integration/test_id_override.das.
Previous tutorial: With style
Boost macros — the macro layer.