Drag tools

Interactive handles you drag inside a plot. drag_point is a draggable (x, y) marker, drag_line_x a draggable vertical line (a threshold), drag_rect a draggable rectangle (a region of interest). Unlike the plot items, a drag tool owns mutable state — the value being dragged — so the caller passes a persistent state struct (a global). The value updates in place each frame and serializes into the snapshot under <plot>/<id>, so a test can read the handle’s screen bbox, synth-drag it, and assert the bound value moved.

var g_pt : DragPointState        // persistent: holds the dragged (x, y)
...
plot(WAVES, (title = "interactive", size = float2(-1.0f, 620.0f), flags = ImPlotFlags.None)) {
    plot_line("sin", g_sin)
    drag_rect(g_roi, "roi", float4(1.0f, 0.0f, 1.0f, 0.35f))
    drag_line_x(g_thresh, "thresh", float4(0.2f, 1.0f, 0.3f, 1.0f), 2.0f)
    drag_point(g_pt, "p0", float4(1.0f, 0.9f, 0.1f, 1.0f), 6.0f)   // drawn last -> grabs first
}

Source: examples/tutorial/drag_tools.das.

 1options gen2
 2
 3require imgui/imgui_harness
 4require imgui/imgui_containers_builtin
 5require imgui/imgui_widgets_builtin
 6require imgui/imgui_implot_boost_v2
 7require implot
 8require math
 9
10// =============================================================================
11// TUTORIAL: drag_tools — interactive draggable handles inside a v2 `plot` scope.
12//
13//   drag_point(state, id, col)   — a draggable (x,y) point.
14//   drag_line_x(state, id, col)  — a draggable vertical line (e.g. a threshold).
15//   drag_rect(state, id, col)    — a draggable rectangle region (e.g. an ROI).
16//
17// Each tool owns a PERSISTENT state struct (a global): its value updates in place
18// as you drag, and serializes into the snapshot under `<plot>/<id>` — so a test can
19// read the handle's bbox, synth-drag it, and assert the bound value moved. Built on
20// the dasImgui harness: runs standalone (windowed), headless (--headless), and live.
21//
22// STANDALONE: daslang.exe modules/dasImguiImplot/examples/tutorial/drag_tools.das
23// LIVE:       daslang-live modules/dasImguiImplot/examples/tutorial/drag_tools.das
24// =============================================================================
25
26var g_ctx : ImPlotContext?
27var g_sin : array<double>
28var g_pt : DragPointState
29var g_thresh : DragLineState
30var g_roi : DragRectState
31
32[export]
33def init() {
34    harness_init("dasImguiImplot — drag_tools", 1100, 760)
35    g_ctx = implot::CreateContext()
36    g_sin <- [for (i in range(200)); double(sin(float(i) * 0.05f))]
37    // Keep the three handles spatially separated so their grab rects never overlap
38    // (an overlapping grab rect steals the active-id and the wrong tool moves).
39    g_pt = DragPointState(x = 150.0lf, y = 0.0lf, held = false)
40    g_thresh = DragLineState(value = 100.0lf, held = false)
41    g_roi = DragRectState(x_min = 20.0lf, y_min = -0.9lf, x_max = 70.0lf, y_max = -0.3lf,
42                          held = false)
43}
44
45[export]
46def update() {
47    if (!harness_begin_frame()) return
48    harness_new_frame()
49
50    SetNextWindowPos(float2(20.0, 20.0), ImGuiCond.Always)
51    SetNextWindowSize(float2(1060.0, 720.0), ImGuiCond.Always)
52    window(PLOT_WIN, (text = "drag tools", closable = false,
53                      flags = ImGuiWindowFlags.None)) {
54        text("Drag the yellow point, the green threshold line, or the magenta ROI box.")
55        plot(WAVES, (title = "interactive", size = float2(-1.0f, 620.0f),
56                     flags = ImPlotFlags.None)) {
57            setup_axes("sample", "value")
58            setup_axes_limits(0.0lf, 200.0lf, -1.5lf, 1.5lf)
59            plot_line("sin", g_sin)
60            // Drawn back-to-front so the point sits on top and grabs first.
61            drag_rect(g_roi, "roi", float4(1.0f, 0.0f, 1.0f, 0.35f))
62            drag_line_x(g_thresh, "thresh", float4(0.2f, 1.0f, 0.3f, 1.0f), 2.0f)
63            drag_point(g_pt, "p0", float4(1.0f, 0.9f, 0.1f, 1.0f), 6.0f)
64        }
65    }
66
67    harness_end_frame()
68}
69
70[export]
71def shutdown() {
72    if (g_ctx != null) {
73        DestroyContext(g_ctx)
74    }
75    harness_shutdown()
76}
77
78[export]
79def main() {
80    init()
81    while (!exit_requested()) {
82        update()
83    }
84    shutdown()
85}

Walkthrough

The recording drives each handle with real synthetic input and self-verifies the bound value moved. The yellow point follows the cursor in x and y; the green threshold line slides along x; and the magenta region shows its two modes — grab the body (near the center) to move the whole rectangle, with all four bounds travelling together, or grab a corner to resize it, where only that corner moves. A missed grab or a frozen handle fails the recording at teardown.

Persistent state

Each tool takes a var state struct the caller keeps alive across frames — DragPointState{x, y, held}, DragLineState{value, held}, DragRectState{x_min, y_min, x_max, y_max, held}. The wrapper writes the new value back through it each frame, and held is true on the frames the handle is grabbed. Initialize them in init and read them wherever you need the current value.

Overlap and draw order

Drag tools grab by an invisible rect around the handle. If two handles’ grab rects overlap, one steals the active-id and the wrong tool moves — so the example keeps the point, the threshold line, and the ROI spatially separated, and draws them back-to-front so the point (drawn last) sits on top and grabs first.

Driving them in tests

The handle’s snapshot path is handle_path(session, id) (e.g. PLOT_WIN/WAVES/p0). The playwright layer reads drag_point_value / drag_line_value / drag_rect_value from the payload, and wait_for_point_moved gates a synthetic drag actually landing — which is how test_drag_tools proves the synthetic drag moves the bound value exactly as a real mouse would.