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 : ClickState declaration to keep in sync with the call site. The macro declares it on first compile.

  • Read anywhereSAVE_BTN.click_count, SPEED.value, etc. are plain daslang globals, readable from any module that requires this one.

  • Dotted flagsIDENT.PUBLIC / IDENT.PRIVATE / IDENT.NOTLIVE tune visibility and live-reload behavior on the emitted global without claiming new syntactic positions in the call.

Source: examples/tutorial/state_telemetry.das.

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
  2
  3require imgui
  4require imgui_app
  5require opengl/opengl_boost
  6require live/glfw_live
  7require live/live_api
  8require live/live_commands
  9require live/live_vars
 10require live_host
 11require imgui/imgui_live
 12require imgui/imgui_boost_runtime
 13require imgui/imgui_boost_v2
 14require imgui/imgui_widgets_builtin
 15require imgui/imgui_containers_builtin
 16require imgui/imgui_visual_aids
 17
 18// =============================================================================
 19// TUTORIAL: state_telemetry — how widget state lives in daslang, not ImGui.
 20//
 21// Each boost widget macro emits a module-scope global named by the first
 22// argument. The global is a typed state struct (ClickState, SliderStateInt,
 23// SliderStateFloat, ToggleState, ...) that holds the widget's value, plus
 24// any pending overrides queued by external drivers. Three immediate wins:
 25//
 26//   1. Auto-emit:        the variable is declared once, by the macro, on
 27//                        first compile. No "var SAVE_BTN : ClickState;"
 28//                        boilerplate to keep in sync with the call site.
 29//   2. Read anywhere:    SAVE_BTN.click_count, SPEED.value are plain
 30//                        globals — readable from any module that requires
 31//                        this one (assuming PUBLIC visibility).
 32//   3. Dotted flags:     IDENT.PUBLIC / IDENT.PRIVATE / IDENT.NOTLIVE
 33//                        modify visibility / live-reload behavior on the
 34//                        emitted global without claiming new syntactic
 35//                        positions in the call.
 36//
 37// Plus `imgui_snapshot` — the registry serializes every registered widget
 38// to JSON: kind, bbox, hex_id, payload (value / click_count / ...). That's
 39// the surface external drivers, integration tests, and visual aids see.
 40//
 41// STANDALONE: daslang.exe modules/dasImgui/examples/tutorial/state_telemetry.das
 42// LIVE:       daslang-live modules/dasImgui/examples/tutorial/state_telemetry.das
 43//
 44// DRIVE (when running live):
 45//   curl -X POST -d '{"name":"imgui_snapshot"}'                                                       localhost:9090/command
 46//   curl -X POST -d '{"name":"imgui_click","args":{"target":"STATE_WIN/SAVE_BTN"}}'                    localhost:9090/command
 47//   curl -X POST -d '{"name":"imgui_force_set","args":{"target":"STATE_WIN/SPEED","value":7}}'               localhost:9090/command
 48//   curl -X POST -d '{"name":"imgui_force_set","args":{"target":"STATE_WIN/STATUS_TEXT","value":"saved"}}'   localhost:9090/command
 49// =============================================================================
 50
 51[export]
 52def init() {
 53    live_create_window("dasImgui state_telemetry tutorial", 1040, 720)
 54    live_imgui_init(live_window)
 55    DisableIniPersistence()
 56    let io & = unsafe(GetIO())
 57    GetStyle().FontScaleMain = 1.5
 58}
 59
 60[export]
 61def update() {
 62    if (!live_begin_frame()) return
 63    begin_frame()
 64
 65    ImGui_ImplGlfw_NewFrame()
 66    apply_synth_io_override()
 67    NewFrame()
 68
 69    SetNextWindowPos(ImVec2(30.0f, 30.0f), ImGuiCond.FirstUseEver)
 70    SetNextWindowSize(ImVec2(640.0f, 460.0f), ImGuiCond.FirstUseEver)
 71    window(STATE_WIN, (text = "state & telemetry", closable = false,
 72                       flags = ImGuiWindowFlags.None)) {
 73
 74        // ---- Auto-emit + read-anywhere ----
 75        // No top-of-file `var SAVE_BTN : ClickState`. The macro emits the
 76        // global the first time it sees `button(SAVE_BTN, ...)`. The
 77        // struct's fields are then read directly:
 78        //   SAVE_BTN.click_count : cumulative (@live → preserved across reload)
 79        //   SAVE_BTN.clicked     : true on the frame the button fired
 80        text("Auto-emit: SAVE_BTN is the macro-emitted global.")
 81        if (button(SAVE_BTN, (text = "Save"))) {
 82            // `button(...)` returns bool — clicked-this-frame. Same info
 83            // as SAVE_BTN.clicked, just inline.
 84        }
 85        text("SAVE_BTN.click_count = {SAVE_BTN.click_count}")
 86
 87        separator(ST_SEP_1)
 88
 89        // ---- Dotted flags ----
 90        // SPEED.PUBLIC: emit the global with `variable public` instead of
 91        // the default `variable private`. Other modules requiring this one
 92        // can then read SPEED.value. Telemetry path stays "SPEED" — flags
 93        // don't leak into the registry path.
 94        //
 95        // VOLUME.NOTLIVE: skip @live on the emitted global. On live-reload
 96        // the source-side initial value wins (helps when you change bounds
 97        // and want them to take effect immediately, not be preserved).
 98        text("Dotted flags tune visibility and live-reload behavior:")
 99        slider_int(SPEED.PUBLIC,   (text = "Speed (int, PUBLIC)"))
100        slider_float(VOLUME.NOTLIVE, (text = "Volume (NOTLIVE)"))
101        text("SPEED.value = {SPEED.value}   VOLUME.value = {VOLUME.value}")
102
103        separator(ST_SEP_2)
104
105        // ---- text_show: app-side value mirror ----
106        // text_show is the read-only mirror of text_input. The state's
107        // .value string is what gets displayed; imgui_force_set can drive it
108        // from outside, and the snapshot exposes it under the standard
109        // payload.value field — so integration tests can assert against
110        // computed status strings the same way they assert slider values.
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 `:= "..."` (clone-string) 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    }
146    shutdown()
147}

Auto-emit

The first time button(SAVE_BTN, ...) is compiled, the macro emits the matching global at module scope:

// emitted automatically — no manual declaration
@live variable private 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.

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"))) { ... }
text("SAVE_BTN.click_count = {SAVE_BTN.click_count}")

Two distinct value channels are available:

  • button(...) returns booltrue on the frame the click fired. Inline-friendly for the “do thing now” case.

  • SAVE_BTN.clicked is 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.

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 as variable public instead of the default variable private. Sibling modules requiring this one can then read SPEED.value directly.

  • VOLUME.NOTLIVE — skip the @live annotation 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.

text_show — the app-driven mirror

text_show is the read-only counterpart to text_inputstate.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()}"
}

The := clones the new string into the current context’s heap — required because daslang-live’s HTTP handler runs in a different context than the GLFW main loop. Plain = would assign a pointer that becomes invalid the moment the request returns.

Standalone vs live

Same convention as previous tutorials.

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

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: examples/tutorial/state_telemetry.das

Richer reference: 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.