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;rotrotates the ellipse aboutcenter(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_segmentsrequired (no adaptive default).add_polyline(dl, points : array<float2>, col, flags = ImDrawFlags.None, thickness = 1.0f)— open or closed polyline throughpoints(useImDrawFlags.Closedto 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). PassImDrawFlags.Closedto 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)— pushcountchannels.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
WidgetEntry — kind ("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.