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

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

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.

Primitives

Eight [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)

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.

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.

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 examples/features/drawlist_path.das for a stroked rounded rect, filled convex pentagon, and filled concave arrow built path-style.

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 examples/features/drawlist_channels.das.

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 examples/features/clip_rect.das.

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", ...)

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

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

Integration tests: tests/integration/test_drawlist_primitives.das, tests/integration/test_with_window_drawlist.das, tests/integration/test_drawlist_path_key.das.

Boost macros — the macro layer.