8.19.38. Drawlist rail

ImGui exposes three viewport-level draw lists — window, foreground, background — plus a family of low-level Add* primitives that paint directly to them (AddLine, AddRect, AddCircle, etc.). The boost layer wraps them behind three scope wrappers and a primitive set covering the geometric basics (line / rect / circle / triangle / text) plus the v2 extras (rect_filled_multi_color / ellipse / ngon / polyline / bezier). The path-building (path_*), channel-splitting (channels_*), and clip-rect rails are control-flow helpers, not registered [drawlist_prim] entries — they shape what the primitives render but do not themselves create snapshot rows:

with_window_drawlist() $(var dl) {
    dl |> add_rect_filled(p0, p1, rgba(40u, 80u, 200u, 200u))
    dl |> add_line(a, b, rgba(255u, 255u, 0u, 255u), 2.0f)
}

Every primitive registers a per-frame snapshot entry under a synthesized <window>/<mod>:<line>:<col> path key, so playwright / mouse-cards can target a specific call site by path without the caller declaring a state global. Drawlist primitives are decoration-only — no hover / active / focus state is captured.

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

8.19.38.1. Walkthrough

The recording is a display-only tour - no gesture to drive - so the self-check is on the output: it narrates each scope wrapper while asserting the registry holds the painted primitives (record_check_kind_count over the add_* kind family, since drawlist call sites register under synthesized path keys rather than caller idents) plus the window / foreground-label widgets on screen. A rail that stopped painting 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_scope_builtin
 19require imgui/imgui_drawlist_builtin
 20require imgui/imgui_colors
 21require imgui/imgui_visual_aids
 22
 23// =============================================================================
 24// TUTORIAL: drawlist — three scope wrappers (with_window_drawlist /
 25//           with_foreground_drawlist / with_background_drawlist) and the
 26//           full primitive set: 8 originals (add_line, add_rect,
 27//           add_rect_filled, add_circle, add_circle_filled, add_triangle,
 28//           add_triangle_filled, add_text) plus the v2 additions
 29//           (add_rect_filled_multi_color, add_ellipse, add_ellipse_filled,
 30//           add_ngon, add_ngon_filled, add_polyline, add_bezier_cubic,
 31//           add_bezier_quadratic). For the path-building, channel-splitting,
 32//           and clip-rect rails see the dedicated feature files
 33//           (examples/features/drawlist_path.das,
 34//           examples/features/drawlist_channels.das,
 35//           examples/features/clip_rect.das).
 36//
 37// The rail bridges to ImGui's three viewport-level draw lists with the same
 38// telemetry surface as `[widget]`: path-keyed entries land in `g_registry`
 39// so mouse-cards / playwright can target a specific call site by
 40// `<window>/<mod>:<line>:<col>`. No per-call state global is registered —
 41// primitives are decoration-only, last write wins on overlapping draws.
 42//
 43// STANDALONE: daslang.exe modules/dasImgui/examples/tutorial/drawlist.das
 44// LIVE:       daslang-live modules/dasImgui/examples/tutorial/drawlist.das
 45// =============================================================================
 46
 47[export]
 48def init() {
 49    live_create_window("dasImgui drawlist tutorial", 1000, 800)
 50    live_imgui_init(live_window)
 51    let io & = unsafe(GetIO())
 52    GetStyle().FontScaleMain = 1.2
 53}
 54
 55[export]
 56def update() { // nolint:STYLE038
 57    if (!live_begin_frame()) return
 58    begin_frame()
 59
 60    ImGui_ImplGlfw_NewFrame()
 61    apply_synth_io_override()
 62    NewFrame()
 63
 64    SetNextWindowPos(ImVec2(30.0f, 30.0f), ImGuiCond.Always)
 65    SetNextWindowSize(ImVec2(700.0f, 700.0f), ImGuiCond.Always)
 66    window(DRAW_WIN, (text = "drawlist - rail tour", closable = false,
 67                      flags = ImGuiWindowFlags.None)) {
 68        text(DRAW_INTRO, (text = "Three scope wrappers + shape primitives (8 originals + v2 extras)."))
 69        separator()
 70
 71        let origin = GetCursorScreenPos()
 72        let red    = rgba(220u,  60u,  60u, 255u)
 73        let green  = rgba(60u, 220u, 100u, 255u)
 74        let blue   = rgba(70u, 130u, 240u, 255u)
 75        let yellow = rgba(240u, 220u,  60u, 255u)
 76        let gray   = rgba(200u, 200u, 210u, 255u)
 77        let bg_tint = rgba(20u, 30u, 50u, 120u)
 78
 79        with_window_drawlist() $(var dl) {
 80            dl |> add_rect_filled(float2(origin.x +   8.0f, origin.y +  8.0f),
 81                                  float2(origin.x + 660.0f, origin.y + 80.0f),
 82                                  rgba(35u, 35u, 50u, 200u), 6.0f)
 83            dl |> add_rect(float2(origin.x +  16.0f, origin.y + 16.0f),
 84                           float2(origin.x + 652.0f, origin.y + 72.0f),
 85                           gray, 4.0f, ImDrawFlags.None, 1.5f)
 86            dl |> add_line(float2(origin.x +  16.0f, origin.y + 16.0f),
 87                           float2(origin.x + 652.0f, origin.y + 72.0f),
 88                           yellow, 2.0f)
 89            dl |> add_circle_filled(float2(origin.x +  60.0f, origin.y + 160.0f), 32.0f, blue, 0)
 90            dl |> add_circle(float2(origin.x + 150.0f, origin.y + 160.0f), 32.0f, blue, 0, 2.0f)
 91            dl |> add_triangle_filled(float2(origin.x + 260.0f, origin.y + 124.0f),
 92                                      float2(origin.x + 226.0f, origin.y + 196.0f),
 93                                      float2(origin.x + 294.0f, origin.y + 196.0f), green)
 94            dl |> add_triangle(float2(origin.x + 380.0f, origin.y + 124.0f),
 95                               float2(origin.x + 346.0f, origin.y + 196.0f),
 96                               float2(origin.x + 414.0f, origin.y + 196.0f), green, 2.0f)
 97            dl |> add_text(float2(origin.x +  16.0f, origin.y + 220.0f), gray,
 98                           "add_text - rendered onto the window drawlist")
 99
100            // ----- v2 additions: shape extras -----
101            // Multi-color gradient rect.
102            dl |> add_rect_filled_multi_color(
103                float2(origin.x +  16.0f, origin.y + 260.0f),
104                float2(origin.x + 240.0f, origin.y + 290.0f),
105                rgba(255u, 0u, 0u, 220u), rgba(0u, 255u, 0u, 220u),
106                rgba(0u, 0u, 255u, 220u), rgba(255u, 255u, 0u, 220u))
107            // Stroked + filled ellipse.
108            dl |> add_ellipse(float2(origin.x + 300.0f, origin.y + 275.0f),
109                              float2(36.0f, 16.0f), yellow, 0.0f, 0, 1.5f)
110            dl |> add_ellipse_filled(float2(origin.x + 400.0f, origin.y + 275.0f),
111                                     float2(36.0f, 16.0f), blue, 0.0f, 0)
112            // Stroked + filled N-gon (hexagon).
113            dl |> add_ngon(float2(origin.x + 500.0f, origin.y + 275.0f),
114                           22.0f, green, 6, 2.0f)
115            dl |> add_ngon_filled(float2(origin.x + 580.0f, origin.y + 275.0f),
116                                  22.0f, green, 6)
117            // Cubic + quadratic bezier curves.
118            dl |> add_bezier_cubic(
119                float2(origin.x +  16.0f, origin.y + 330.0f),
120                float2(origin.x + 110.0f, origin.y + 290.0f),
121                float2(origin.x + 200.0f, origin.y + 360.0f),
122                float2(origin.x + 300.0f, origin.y + 310.0f),
123                yellow, 2.0f, 0)
124            dl |> add_bezier_quadratic(
125                float2(origin.x + 320.0f, origin.y + 330.0f),
126                float2(origin.x + 430.0f, origin.y + 290.0f),
127                float2(origin.x + 540.0f, origin.y + 330.0f),
128                blue, 2.0f, 0)
129            // Polyline (open M-shape) below.
130            var poly <- [
131                float2(origin.x +  16.0f, origin.y + 380.0f),
132                float2(origin.x +  60.0f, origin.y + 350.0f),
133                float2(origin.x + 100.0f, origin.y + 370.0f),
134                float2(origin.x + 140.0f, origin.y + 350.0f),
135                float2(origin.x + 180.0f, origin.y + 380.0f)
136            ]
137            dl |> add_polyline(poly, gray, ImDrawFlags.None, 1.5f)
138            delete poly
139        }
140
141        dummy(DRAW_BAND_SLOT, (size = float2(660.0f, 400.0f)))
142
143        text(DRAW_FG_LABEL, (text = "Foreground swatch (overlays everything):"))
144
145        let vp_pos = GetMainViewport().Pos
146        let vp_size = GetMainViewport().Size
147        with_foreground_drawlist() $(var fdl) {
148            fdl |> add_rect_filled(float2(vp_pos.x + 12.0f, vp_pos.y + 12.0f),
149                                   float2(vp_pos.x + 100.0f, vp_pos.y + 26.0f), red)
150            fdl |> add_text(float2(vp_pos.x + 20.0f, vp_pos.y + 12.0f),
151                            rgba(255u, 255u, 255u, 255u), "FG")
152        }
153
154        with_background_drawlist() $(var bdl) {
155            bdl |> add_rect_filled(float2(vp_pos.x, vp_pos.y),
156                                   float2(vp_pos.x + vp_size.x, vp_pos.y + vp_size.y),
157                                   bg_tint)
158        }
159    }
160
161    end_of_frame()
162    Render()
163    var w, h : int
164    live_get_framebuffer_size(w, h)
165    glViewport(0, 0, w, h)
166    glClearColor(0.10f, 0.10f, 0.12f, 1.0f)
167    glClear(GL_COLOR_BUFFER_BIT)
168    live_imgui_render()
169
170    live_end_frame()
171}
172
173[export]
174def shutdown() {
175    live_imgui_shutdown()
176    live_destroy_window()
177}
178
179[export]
180def main() {
181    init()
182    while (!exit_requested()) {
183        update()
184        maybe_collect_gc()
185    }
186    shutdown()
187}

8.19.38.1.1. Scope wrappers

Three thin wrappers cover the three drawlist scopes:

  • with_window_drawlist() $(var dl) { ... } — current window’s drawlist. Painting is clipped to the window’s content region and stacks under any subsequent widgets rendered in this window.

  • with_foreground_drawlist() $(var dl) { ... } — top-most foreground drawlist. Paints over every window on the viewport. Use for HUD overlays, drag indicators, tooltips that must escape window clipping.

  • with_background_drawlist() $(var dl) { ... } — bottom-most background drawlist. Paints under every window on the viewport. Use for full-viewport backdrops (gradients, vignettes, debug grids).

Each wrapper acquires the drawlist pointer, yields it to the block as dl, and exits. There’s no push / pop pairing under the hood — drawlists are viewport-level state that any ImGui code can reach via GetWindowDrawList / GetForegroundDrawList / GetBackgroundDrawList; the wrappers exist to give the rail a uniform body shape (with_X() $(var dl) { dl |> add_*(...) }) and to keep raw GetWindowDrawList calls off the boost surface.

8.19.38.1.2. Primitives

Nine [drawlist_prim]-tagged painters cover the geometric basics:

  • add_line(dl, a, b, col, thickness = 1.0f)

  • add_rect(dl, a, b, col, rounding = 0.0f, flags = ImDrawFlags.None, thickness = 1.0f)

  • add_rect_filled(dl, a, b, col, rounding = 0.0f, flags = ImDrawFlags.None)

  • add_circle(dl, center, radius, col, num_segments = 0, thickness = 1.0f)

  • add_circle_filled(dl, center, radius, col, num_segments = 0)

  • add_triangle(dl, a, b, c, col, thickness = 1.0f)

  • add_triangle_filled(dl, a, b, c, col)

  • add_text(dl, pos, col, text)

  • add_text_clipped(dl, pos, col, text, clip_rect) — same glyph run with a per-call CPU-side clip (xmin, ymin, xmax, ymax); rasterizes only the glyphs that intersect it, and touches no clip stack.

col is a packed ABGR uint — use rgba(r, g, b, a) to build one, or GetColorU32(ImGuiCol.*) for style-colour references. Positions are screen-space pixels; widget-relative drawing typically anchors on GetCursorScreenPos / GetItemRectMin / GetItemRectMax.

8.19.38.1.3. Shape extras (v2 additions)

Eight additional [drawlist_prim] painters round out the shape set:

  • add_rect_filled_multi_color(dl, a, b, col_ul, col_ur, col_br, col_bl) — per-corner gradient (4 colours, bilinearly interpolated).

  • add_ellipse(dl, center, radii, col, rot = 0.0f, num_segments = 0, thickness = 1.0f) / add_ellipse_filled(dl, center, radii, col, rot = 0.0f, num_segments = 0)radii = (rx, ry) half-axes; rot rotates the ellipse about center (radians).

  • add_ngon(dl, center, radius, col, num_segments, thickness = 1.0f) / add_ngon_filled(dl, center, radius, col, num_segments) — regular N-gons; num_segments required (no adaptive default).

  • add_polyline(dl, points : array<float2>, col, flags = ImDrawFlags.None, thickness = 1.0f) — open or closed polyline through points (use ImDrawFlags.Closed to close).

  • add_bezier_cubic(dl, p1, p2, p3, p4, col, thickness = 1.0f, num_segments = 0) / add_bezier_quadratic(dl, p1, p2, p3, col, thickness = 1.0f, num_segments = 0) — bezier curves through anchor + control points.

8.19.38.1.4. Path building

For complex one-off shapes (rounded rects assembled from arcs, custom polygons, etc.) the path family lets the caller build a vertex stack incrementally then terminate with one of path_stroke / path_fill_convex / path_fill_concave:

  • path_clear(dl) — discard pending path vertices.

  • path_line_to(dl, pos) — append a straight-line segment.

  • path_arc_to(dl, center, radius, a_min, a_max, num_segments = 0) — append a circular arc.

  • path_bezier_quadratic_curve_to(dl, p2, p3, num_segments = 0) — append a quadratic bezier curve from the path’s current endpoint.

  • path_fill_convex(dl, col) / path_fill_concave(dl, col) — close + fill (convex is faster; concave handles non-convex polygons).

  • path_stroke(dl, col, flags = ImDrawFlags.None, thickness = 1.0f) — stroke the pending path (open by default). Pass ImDrawFlags.Closed to join the last vertex back to the first.

See modules/dasImgui/examples/features/drawlist_path.das for a stroked rounded rect, filled convex pentagon, and filled concave arrow built path-style.

8.19.38.1.5. Channel splitting

ImGui drawlists are append-only — once a vertex lands, later draws sit on top. channels_split defers ordering: split the drawlist into N channels, submit each layer in convenient order, then channels_merge flattens them in channel-index order (channel 0 first, channel N-1 last).

  • channels_split(dl, count) — push count channels.

  • channels_set_current(dl, idx) — switch the active channel.

  • channels_merge(dl) — flatten back to the main draw stream.

Typical use: draw a card’s foreground content first to measure its bounding box, then go back to channel 0 to paint the background card behind it. See modules/dasImgui/examples/features/drawlist_channels.das.

8.19.38.1.6. Clip rect scope

with_clip_rect(min, max, intersect_with_current) { ... } (lives in imgui_scope_builtin) restricts every widget and drawlist primitive submitted inside the block to the given screen-space rect. Set intersect_with_current=true (typical) to intersect with the existing clip; false replaces it. See modules/dasImgui/examples/features/clip_rect.das.

To clip one drawlist only — leaving ImGui’s widget hit-testing and the other drawlists alone — use with_drawlist_clip_rect(dl, min, max, intersect_with_current) { ... } from imgui_drawlist_builtin: it pushes and pops on that drawlist’s own clip stack.

8.19.38.1.7. Path-key telemetry

The [drawlist_prim] annotation synthesizes a stable <mod>:<line>:<col> path key at every call site (no per-call state global is registered). Each primitive’s body publishes a lightweight WidgetEntrykind ("add_line" / "add_rect" / …) plus bbox — into the per-frame registry under that key. Playwright / mouse-cards can then target a specific call site by path:

var snap = wait_for_widget(d, "MY_WIN/<mod>:42:8", 15.0f)
t |> equal(find_widget(snap, "MY_WIN/<mod>:42:8")?["kind"] ?? "",
           "add_rect", "the call site painted a rect")

Because the synthesized key shifts on edits, tests typically enumerate drawlist entries by kind ("add_rect" etc.) rather than hardcoding the line / column — see modules/dasImgui/tests/test_drawlist_path_key.das for the walker pattern.

8.19.38.1.8. Standalone vs live

Same convention as the other tutorials. daslang.exe runs the demo headlessly to frame N; daslang-live keeps the window open and reloads on source edits.

See also

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

Integration tests: modules/dasImgui/tests/test_drawlist_primitives.das, modules/dasImgui/tests/test_with_window_drawlist.das, modules/dasImgui/tests/test_drawlist_path_key.das.

Boost macros — the macro layer.