8.19.13. State & telemetry
Widget state lives in daslang, not in ImGui. Every boost widget macro
emits a module-scope global named by the first argument — typed as
ClickState / SliderStateFloat / ToggleState / etc. The
global holds the widget’s value plus any pending overrides queued by
external drivers. The same global is what the registry serializes for
imgui_snapshot, so the daslang side, the test side, and the
external-driver side all see the same value.
Three immediate wins fall out of that design:
Auto-emit — no
var SAVE_BTN : ClickStatedeclaration to keep in sync with the call site. The macro declares it on first compile.Read anywhere —
SAVE_BTN.click_count,SPEED.value, etc. are plain daslang globals, readable from any module that requires this one.Dotted flags —
IDENT.PUBLIC/IDENT.PRIVATE/IDENT.NOTLIVEtune visibility and live-reload behavior on the emitted global without claiming new syntactic positions in the call.
Source: modules/dasImgui/examples/tutorial/state_telemetry.das.
8.19.13.1. Walkthrough
The recording drives every channel and asserts the snapshot followed: it clicks
Save twice and verifies SAVE_BTN.click_count (hold_through_voice),
force-sets SPEED and VOLUME from outside and verifies each value
(force_set_verified), force-sets STATUS_TEXT and verifies the mirror took
the string, then clicks bump and verifies the app rewrote STATUS_TEXT.value
from inside (record_check_changed). Any channel that stopped reaching the
snapshot would abort the recording.
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_visual_aids
19
20// =============================================================================
21// TUTORIAL: state_telemetry — how widget state lives in daslang, not ImGui.
22//
23// Each boost widget macro emits a module-scope global named by the first
24// argument. The global is a typed state struct (ClickState, SliderStateInt,
25// SliderStateFloat, ToggleState, ...) that holds the widget's value, plus
26// any pending overrides queued by external drivers. Three immediate wins:
27//
28// 1. Auto-emit: the variable is declared once, by the macro, on
29// first compile. No "var SAVE_BTN : ClickState;"
30// boilerplate to keep in sync with the call site.
31// 2. Read anywhere: SAVE_BTN.click_count, SPEED.value are plain
32// globals — readable from any module that requires
33// this one (assuming PUBLIC visibility).
34// 3. Dotted flags: IDENT.PUBLIC / IDENT.PRIVATE / IDENT.NOTLIVE
35// modify visibility / live-reload behavior on the
36// emitted global without claiming new syntactic
37// positions in the call.
38//
39// Plus `imgui_snapshot` — the registry serializes every registered widget
40// to JSON: kind, bbox, hex_id, payload (value / click_count / ...). That's
41// the surface external drivers, integration tests, and visual aids see.
42//
43// STANDALONE: daslang.exe modules/dasImgui/examples/tutorial/state_telemetry.das
44// LIVE: daslang-live modules/dasImgui/examples/tutorial/state_telemetry.das
45//
46// DRIVE (when running live):
47// curl -X POST -d '{"name":"imgui_snapshot"}' localhost:9090/command
48// curl -X POST -d '{"name":"imgui_click","args":{"target":"STATE_WIN/SAVE_BTN"}}' localhost:9090/command
49// curl -X POST -d '{"name":"imgui_force_set","args":{"target":"STATE_WIN/SPEED","value":7}}' localhost:9090/command
50// curl -X POST -d '{"name":"imgui_force_set","args":{"target":"STATE_WIN/STATUS_TEXT","value":"saved"}}' localhost:9090/command
51// =============================================================================
52
53[export]
54def init() {
55 live_create_window("dasImgui state_telemetry tutorial", 1040, 720)
56 live_imgui_init(live_window)
57 DisableIniPersistence()
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, 460.0f), ImGuiCond.FirstUseEver)
73 window(STATE_WIN, (text = "state & telemetry", closable = false,
74 flags = ImGuiWindowFlags.None)) {
75
76 // ---- Auto-emit + read-anywhere ----
77
78 // No top-of-file `var SAVE_BTN : ClickState` — the macro emits the global
79 // on first `button(SAVE_BTN, ...)`. Read its fields directly: .click_count
80 // (cumulative, @live-preserved) and .clicked (true the frame it fired).
81 text("Auto-emit: SAVE_BTN is the macro-emitted global.")
82 if (button(SAVE_BTN, (text = "Save"))) {
83 // `button(...)` returns bool — clicked-this-frame. Same info
84 // as SAVE_BTN.clicked, just inline.
85 }
86 text("SAVE_BTN.click_count = {SAVE_BTN.click_count}")
87
88 separator(ST_SEP_1)
89
90 // ---- Dotted flags ----
91
92 // SPEED.PUBLIC: emit the global `var public` (not the default private)
93 // so requiring modules can read SPEED.value. Path stays "SPEED" — flags
94 // don't leak into the registry path.
95
96 // VOLUME.NOTLIVE: skip @live on the emitted global, so on reload the
97 // source-side initial value wins (useful when you change bounds and want
98 // them to take effect immediately rather than be preserved).
99 text("Dotted flags tune visibility and live-reload behavior:")
100 slider_int(SPEED.PUBLIC, (text = "Speed (int, PUBLIC)"))
101 slider_float(VOLUME.NOTLIVE, (text = "Volume (NOTLIVE)"))
102 text("SPEED.value = {SPEED.value} VOLUME.value = {VOLUME.value}")
103
104 separator(ST_SEP_2)
105
106 // ---- text_show: app-side value mirror ----
107
108 // text_show is the read-only mirror of text_input: its state.value string
109 // is displayed, imgui_force_set drives it from outside, and the snapshot
110 // exposes it under payload.value so tests can assert computed strings.
111 text("text_show - app-driven status reaches the snapshot:")
112 text_show(STATUS_TEXT)
113
114 // The bump button writes a computed string into STATUS_TEXT.value.
115 // Both `= "..."` and external imgui_force_set work; the
116 // snapshot reflects whichever ran most recently.
117 if (button(BUMP_STATUS, (text = "bump status"))) {
118 STATUS_TEXT.value = "saved at frame {get_uptime()}"
119 }
120 }
121
122 end_of_frame()
123 Render()
124 var w, h : int
125 live_get_framebuffer_size(w, h)
126 glViewport(0, 0, w, h)
127 glClearColor(0.10f, 0.10f, 0.12f, 1.0f)
128 glClear(GL_COLOR_BUFFER_BIT)
129 live_imgui_render()
130
131 live_end_frame()
132}
133
134[export]
135def shutdown() {
136 live_imgui_shutdown()
137 live_destroy_window()
138}
139
140[export]
141def main() {
142 init()
143 while (!exit_requested()) {
144 update()
145 maybe_collect_gc()
146 }
147 shutdown()
148}
8.19.13.1.1. Auto-emit
The first time button(SAVE_BTN, ...) is compiled, the macro emits
the matching global at module scope:
// emitted automatically — no manual declaration
var private @live SAVE_BTN : ClickState = ClickState()
That’s why there’s no var SAVE_BTN at the top of the file. The
state struct is owned by daslang — visible to grep, walkable via
RTTI, persistable through the standard serializer, preserved across
daslang-live reloads thanks to @live.
8.19.13.1.2. Reading state
Once emitted, the global behaves like any other daslang global —
SAVE_BTN.click_count is a plain field access:
if (button(SAVE_BTN, (text = "Save"))) {
// clicked this frame — same information as SAVE_BTN.clicked
}
text("SAVE_BTN.click_count = {SAVE_BTN.click_count}")
Two distinct value channels are available:
button(...)returnsbool—trueon the frame the click fired. Inline-friendly for the “do thing now” case.SAVE_BTN.clickedis the same flag, surfaced as a field. Useful when the click handler is far from the call site, or in another module that requires this one.
Cumulative counters (click_count for buttons, changed for
sliders, etc.) live alongside on the state struct. Walk
imgui_boost_runtime.das for the full field list per state struct.
8.19.13.1.3. Dotted flags
A dot suffix on the identifier flips flags on the emitted global. The
telemetry path uses only the bare identifier (STATE_WIN/SPEED,
never STATE_WIN/SPEED.PUBLIC) — flags never leak into the path or
the ImGui hash.
SPEED.PUBLIC— emit asvar publicinstead of the defaultvar private. Sibling modules requiring this one can then readSPEED.valuedirectly.VOLUME.NOTLIVE— skip the@liveannotation on the emitted global. Useful when you change the slider bounds and want the source-side initial value to take effect immediately on reload rather than be preserved.IDENT.PRIVATE— explicit default (same as no suffix). Lists cleanly when you grep for visibility intent.
Multiple flags compose: RPS.PUBLIC.NOTLIVE emits a public,
non-@live global. New flags can land on demand without affecting
the call syntax.
8.19.13.1.4. text_show — the app-driven mirror
text_show is the read-only counterpart to text_input —
state.value is what the widget renders, and the value can be
written by the app (STATUS_TEXT.value = "...") or by an
external driver (imgui_force_set with a string value). Either way the
snapshot exposes the current value under the standard
payload.value field, so integration tests can assert on computed
status strings the same way they assert slider values:
text_show(STATUS_TEXT)
if (button(BUMP_STATUS, (text = "bump status"))) {
STATUS_TEXT.value = "saved at frame {get_uptime()}"
}
Plain = is right here: the interpolated string is freshly built on
the app’s own heap, in the same context that renders the frame. The
external path never touches this assignment —
imgui_force_set hands the JSON string to the widget dispatcher,
which stores it on the state struct for the next frame to render.
8.19.13.1.5. Standalone vs live
Same convention as previous tutorials.
8.19.13.1.6. Driving from outside
The snapshot exposes the state structs as JSON:
curl -X POST -d '{"name":"imgui_snapshot"}' localhost:9090/command
# Excerpt of the response:
# "globals": {
# "STATE_WIN/SAVE_BTN": { "kind": "button", "payload": {"click_count": 2}, ... },
# "STATE_WIN/SPEED": { "kind": "slider_int", "payload": {"value": 7, ...}, ... },
# "STATE_WIN/STATUS_TEXT": { "kind": "text_show", "payload": {"value": "saved at frame 12.3"}, ... }
# }
Drivers go through the same registry — imgui_force_set looks up the
target, queues the pending value on the matching state struct, and
the renderer consumes it next frame:
curl -X POST -d '{"name":"imgui_force_set","args":{"target":"STATE_WIN/SPEED","value":7}}' \
localhost:9090/command
curl -X POST -d '{"name":"imgui_force_set","args":{"target":"STATE_WIN/STATUS_TEXT","value":"hello"}}' \
localhost:9090/command
curl -X POST -d '{"name":"imgui_click","args":{"target":"STATE_WIN/SAVE_BTN"}}' \
localhost:9090/command
8.19.13.1.7. Next steps
So far every tutorial has used a single window(...) container.
Containers come next — modal dialogs, popups, tab bars, child windows,
and menus, all sharing the same block-arg pattern.
See also
Full source: modules/dasImgui/examples/tutorial/state_telemetry.das
Richer reference: modules/dasImgui/examples/features/foundation.das — the
features-side demo that established the auto-emit + dotted-flag
surface plus the unified L2/L3 dispatch.
Snapshot contract: see imgui_boost_runtime.das for the per-kind
state_struct definitions (ClickState, SliderStateInt,
SliderStateFloat, ToggleState, TextShowState, …).
Previous tutorial: With id
Boost macros — the macro layer.