8.19.24. Drag and drop

ImGui’s drag-drop API is two paired Begin*/End* calls: one attached to the previously submitted item that’s being dragged, one attached to the previously submitted item that’s receiving. Both Begin* calls return false unless ImGui’s internal drag state machine says “drag-active” or “hover-over-target with payload”; calling End* when Begin* returned false is undefined behavior.

The boost layer ships two [container]``s ``drag_drop_source and drag_drop_target — that gate their bodies on the corresponding Begin* return. Payload set/accept stay raw calls inside the body because the data is caller-owned and crosses widget boundaries.

Source: modules/dasImgui/examples/features/drag_drop.das.

8.19.24.1. Walkthrough

The recording introduces the source button and the drop target, then performs a real drag of the source onto the target. On release the target accepts the MY_INT payload — Target received value flips to 42 and Drops accepted ticks to 1. The drag is synthesized through the same synth-mouse pipeline a test would use, and the recording asserts the drop actually landed (see Driving from outside below).

 1options gen2
 2options _comment_hygiene = true
 3options gc
 4
 5require imgui/imgui_harness
 6
 7// =============================================================================
 8// FEATURE: drag_drop — [container]s drag_drop_source + drag_drop_target.
 9//          Both gate their body on ImGui's internal drag-drop state machine.
10//          Bodies call raw SetDragDropPayload / AcceptDragDropPayload —
11//          payload data is caller-owned and crosses widget boundaries.
12// SHOWS:   drag SOURCE_BTN onto TARGET_BTN; TARGET_BTN's body accepts the
13//          "MY_INT" payload and writes it into RECEIVED. DROP_COUNT
14//          increments on each successful release-over-target.
15// STANDALONE: daslang.exe modules/dasImgui/examples/features/drag_drop.das
16// HEADLESS:   daslang.exe modules/dasImgui/examples/features/drag_drop.das -- --headless --headless-frames=60
17// LIVE:       daslang-live modules/dasImgui/examples/features/drag_drop.das
18// =============================================================================
19
20var PAYLOAD_VALUE : int = 42
21var RECEIVED : int = 0
22var DROP_COUNT : int = 0
23
24[export]
25def init() {
26    harness_init("drag_drop - source + target", 720, 480)
27    let io & = unsafe(GetIO())
28    GetStyle().FontScaleMain = 1.5
29}
30
31[export]
32def update() {
33    if (!harness_begin_frame()) return
34    harness_new_frame()
35
36    SetNextWindowSize(ImVec2(660.0, 420.0), ImGuiCond.Always)
37    window(MAIN_WIN, (text = "drag_drop", closable = false,
38                      flags = ImGuiWindowFlags.None)) {
39        text("Drag the SOURCE button onto the TARGET button.")
40        separator()
41
42        // Source: a normal button. Body runs only on active drag.
43        button(SOURCE_BTN, (text = "SOURCE"))
44        drag_drop_source(SOURCE_DD, (flags = ImGuiDragDropFlags.None)) {
45            unsafe {
46                SetDragDropPayload("MY_INT", addr(PAYLOAD_VALUE),
47                                   uint64(typeinfo sizeof(PAYLOAD_VALUE)),
48                                   ImGuiCond.Once)
49            }
50            text("Dragging int: {PAYLOAD_VALUE}")
51        }
52
53        // 40 px gap: same_line's named-tuple destructure is POSITIONAL
54        // (offset_from_start_x, spacing). `(spacing = 40.0f)` alone binds 40 to
55        // offset and overlaps TARGET onto SOURCE, so the drag never starts.
56        same_line((offset_from_start_x = 0.0f, spacing = 40.0f))
57
58        // Target: a normal button. Body runs only when payload-bearing drag hovers.
59        button(TARGET_BTN, (text = "TARGET"))
60        drag_drop_target(TARGET_DD) {
61            let payload = AcceptDragDropPayload("MY_INT", ImGuiDragDropFlags.None)
62            if (payload != null) {
63                unsafe {
64                    RECEIVED = (reinterpret<int? const>(payload.Data))[0]
65                }
66                DROP_COUNT++
67            }
68        }
69
70        separator()
71        text("Source payload value: {PAYLOAD_VALUE}")
72        text("Target received value: {RECEIVED}")
73        text("Drops accepted: {DROP_COUNT}")
74    }
75
76    harness_end_frame()
77}
78
79[export]
80def shutdown() {
81    harness_shutdown()
82}
83
84[export]
85def main() {
86    init()
87    while (!exit_requested()) {
88        update()
89        harness_maybe_collect_gc()
90    }
91    shutdown()
92}

8.19.24.1.1. Requires

Same backend + boost layer as the other container tutorials, plus the drag_drop_source / drag_drop_target macros from imgui/imgui_containers_builtin (which is already required by the container family).

8.19.24.1.2. Source side

Attach a drag_drop_source immediately after the source widget. ImGui implicitly binds it to the previously submitted item:

button(SOURCE_BTN, (text = "SOURCE"))
drag_drop_source(SOURCE_DD, (flags = ImGuiDragDropFlags.None)) {
    unsafe {
        SetDragDropPayload("MY_INT", addr(PAYLOAD_VALUE),
                           uint64(typeinfo sizeof(PAYLOAD_VALUE)),
                           ImGuiCond.Once)
    }
    text("Dragging: {PAYLOAD_VALUE}")
}

The body runs only while the drag is active — after the user has pressed the mouse on SOURCE_BTN and dragged past ImGui’s io.MouseDragThreshold. Inside the body:

  • SetDragDropPayload(type, data, size, cond) publishes the payload. type is a short string (max 32 chars) that the target compares against; data is a raw pointer with caller-controlled lifetime (ImGui copies it into its internal buffer on each call, so a stack pointer is fine inside the body).

  • Any rendering call (text, image, etc.) draws the drag preview tooltip that follows the cursor.

8.19.24.1.3. Target side

Attach a drag_drop_target immediately after the target widget:

button(TARGET_BTN, (text = "TARGET"))
drag_drop_target(TARGET_DD) {
    let payload = AcceptDragDropPayload("MY_INT", ImGuiDragDropFlags.None)
    if (payload != null) {
        unsafe {
            RECEIVED = (reinterpret<int? const>(payload.Data))[0]
        }
    }
}

The body runs only while a drag is hovering this widget. Inside:

  • AcceptDragDropPayload(type, flags) returns a non-null ImGuiPayload? on the frame the user releases over the target with a payload whose type matches type; otherwise it returns null (including on every frame the drag is hovering but not yet dropped).

  • The returned payload’s Data field is a void? constreinterpret<T? const>(payload.Data)[0] reads the typed value back.

8.19.24.1.4. Driving from outside

Playwright exposes a high-level drag_to that bridges source-bbox → target-bbox automatically:

require imgui/imgui_playwright
drag_to(app, "MAIN_WIN/SOURCE_BTN", "MAIN_WIN/TARGET_BTN", steps = 8)

It reads both bboxes from a fresh snapshot, computes (dx, dy) = target_center - source_center, and composes an imgui_mouse_play timeline — warp to the source, press, steps-scaled interpolated moves to the target, release. ImGui’s drag state machine activates around the second or third move (depending on io.MouseDragThreshold) and resolves on release.

The lower-level drag(app, target, dx, dy, steps) is available when you want absolute offsets — useful for testing drag-without-drop or drag-cancel paths.

Verifying the drop. drag_drop_target surfaces a running accepted count in its telemetry payload — it counts the drops ImGui actually delivers (GetDragDropPayload().Delivery), so a driver or test can assert the drop landed without reaching into the example’s caller-owned globals:

drag_to(app, "MAIN_WIN/SOURCE_BTN", "MAIN_WIN/TARGET_BTN")
// accepted ticks 0 -> 1 on a real delivery over the target
wait_for_payload_value(app, "MAIN_WIN/TARGET_DD", "accepted", 1, 300)

8.19.24.1.5. Next steps

That’s the last container tutorial. The next walkthrough switches focus to live-reload internals.

See also

Full source: modules/dasImgui/examples/features/drag_drop.das

Integration test: modules/dasImgui/tests/test_drag_drop.das.

Boost macros — the macro layer.

Containers — the container family in general.