Create by drag

The connect by drag tutorial dragged from a pin onto another pin to make a link. Release that same drag in empty canvas instead and the editor offers to create a node there, pre-wired to the pin you started from. The create scope reports it through show_new_node_drag: it hands back the source pin and the canvas-space drop point, the app opens its create-node menu, and on a pick it spawns the node at the drop point and enqueue_new_link connects the source to it.

var drag_fired = false
begin_create(g_ed) {
    var a = 0
    var b = 0
    if (query_new_link(g_ed, a, b)) {                  // pin -> pin: commit a link
        if (a != 0 && b != 0 && a != b && accept_new_item(g_ed)) {
            commit_link(a, b)
        }
    }
    if (show_new_node_drag(g_ed, g_drag_pin, g_drop_pos)) {   // pin -> empty
        drag_fired = true                              // remember source + drop point
    }
}
with_suspended() {                                     // the menu is screen-space ImGui
    if (drag_fired) {
        open_popup("ne_create")
    }
    popup_window(CREATE_MENU, (str_id = "ne_create")) {
        if (menu_label(ADD_MUL, (text = "Multiply"))) {
            spawn_and_connect("Multiply")              // spawn + enqueue_new_link
        }
    }
}

Source: examples/tutorial/create_by_drag.das.

Walkthrough

The recording is voiced and self-verifying: a real synthetic pin-drag released in empty canvas must open the create menu, and the menu pick must spawn the node and commit the auto-link (a no-op aborts at teardown). It closes by pulsing the freshly enqueued link with flow() to show the editor-made wire is live.

  1options gen2
  2
  3require imgui/imgui_harness
  4require imgui/imgui_node_editor_boost_v2
  5require imgui/imgui_node_editor_live
  6
  7// =============================================================================
  8// TUTORIAL: create_by_drag — drag from a pin into EMPTY canvas to create + connect.
  9//
 10//   begin_create(ed) {
 11//       if (query_new_link(ed, a, b)) { ... accept_new_item ... }   -- pin -> pin
 12//       if (show_new_node_drag(ed, from_pin, drop_pos)) {           -- pin -> empty
 13//           drag_fired = true                                        -- (remember source + pos)
 14//       }
 15//   }
 16//   with_suspended() {                                              -- popups in screen space
 17//       if (drag_fired) open_popup("...")
 18//       popup_window(...) { ... menu_label(...) -> spawn + enqueue_new_link ... }
 19//   }
 20//
 21// connect_by_drag taught the pin -> pin gesture. Releasing the SAME drag in empty
 22// canvas instead is the "create + connect" gesture: show_new_node_drag hands back the
 23// source pin + the canvas-space drop point, the app opens its create-node menu, and on
 24// a pick it spawns at the drop point and enqueue_new_link's the source to the new pin.
 25// The enqueued link replays through begin_create's query_new_link a frame later — the
 26// same path a hand-dragged link takes — so commit_link stores it once.
 27//
 28// STANDALONE: daslang.exe modules/dasImguiNodeEditor/examples/tutorial/create_by_drag.das
 29// LIVE:       daslang-live modules/dasImguiNodeEditor/examples/tutorial/create_by_drag.das
 30// =============================================================================
 31
 32struct Nd {
 33    id      : int
 34    title   : string
 35    in_pin  : int
 36    out_pin : int
 37    pos     : float2
 38}
 39
 40struct Lk {
 41    id       : int
 42    from_pin : int
 43    to_pin   : int
 44}
 45
 46var g_nodes    : table<int; Nd>
 47var g_links    : table<int; Lk>
 48var g_ed       : imgui_node_editor::EditorContext? = null
 49var g_seeded   : bool = false
 50var g_next_id  : int = 200      // ids for drag-created nodes (pins follow: in = id+1, out = id+2)
 51var g_next_link : int = 100
 52var g_drag_pin : int = 0        // source pin of a pin->empty drag (0 = none pending)
 53var g_drop_pos : float2         // canvas pos where the drag released
 54
 55var NODE_TITLE : table<int; NarrativeState>   // per-id title slot (data-driven node idiom)
 56
 57def seed() {
 58    // One source node with a single output pin to drag from.
 59    g_nodes[1] = Nd(id = 1, title = "Source", in_pin = 0, out_pin = 11, pos = float2(140.0, 230.0))
 60}
 61
 62def commit_link(from_out : int; to_in : int) {
 63    let lid = g_next_link
 64    g_next_link ++
 65    g_links[lid] = Lk(id = lid, from_pin = from_out, to_pin = to_in)
 66}
 67
 68def spawn_node(kind : string; pos : float2) : int {
 69    // Each drag-created node is a downstream sink: one input pin (id+1), one output (id+2).
 70    let nid = g_next_id
 71    g_next_id += 10
 72    g_nodes[nid] = Nd(id = nid, title = kind, in_pin = nid + 1, out_pin = nid + 2, pos = pos)
 73    imgui_node_editor::SetNodePosition(nid, pos)   // inside the editor block -> raw SetNodePosition
 74    return nid
 75}
 76
 77def spawn_and_connect(kind : string) {
 78    let nid = spawn_node(kind, g_drop_pos)
 79    // Auto-connect: enqueue source -> new input. The link replays through begin_create's
 80    // query_new_link next frame (commit_link stores it) — never added to g_links directly.
 81    if (g_drag_pin != 0) {
 82        enqueue_new_link(g_ed, g_drag_pin, g_nodes[nid].in_pin)
 83        g_drag_pin = 0
 84    }
 85}
 86
 87def draw_editor() {
 88    node_editor("graph", (editor = g_ed)) {
 89        if (!g_seeded) {
 90            for (n in values(g_nodes)) {
 91                imgui_node_editor::SetNodePosition(n.id, n.pos)
 92            }
 93            g_seeded = true
 94        }
 95        for (n in values(g_nodes)) {
 96            node(n.id) {
 97                text(NODE_TITLE[n.id], (text = n.title))
 98                if (n.in_pin != 0) {
 99                    pin(n.in_pin, PinKind.Input) {
100                        text("-> in")
101                    }
102                }
103                if (n.out_pin != 0) {
104                    pin(n.out_pin, PinKind.Output) {
105                        text("out ->")
106                    }
107                }
108            }
109        }
110        for (l in values(g_links)) {
111            link(l.id, l.from_pin, l.to_pin)
112        }
113        // The create scope serves both gestures. Pins arrive output-first (a = output,
114        // b = input) for a live pin->pin drag AND for the enqueue_new_link replay, so the
115        // auto-link from a menu pick lands here too.
116        var drag_fired = false
117        begin_create(g_ed) {
118            var a = 0
119            var b = 0
120            if (query_new_link(g_ed, a, b)) {
121                if (a != 0 && b != 0 && a != b && accept_new_item(g_ed)) {
122                    commit_link(a, b)
123                }
124            }
125            if (show_new_node_drag(g_ed, g_drag_pin, g_drop_pos)) {
126                drag_fired = true
127            }
128        }
129        // The create-node menu is plain ImGui (screen space) -> Suspend/Resume island.
130        with_suspended() {
131            if (drag_fired) {
132                open_popup("ne_create")
133            }
134            popup_window(CREATE_MENU, (str_id = "ne_create", flags = ImGuiWindowFlags.None)) {
135                text("Create + connect")
136                if (menu_label(ADD_MUL, (text = "Multiply"))) {
137                    spawn_and_connect("Multiply")
138                }
139                if (menu_label(ADD_OUT, (text = "Output"))) {
140                    spawn_and_connect("Output")
141                }
142            }
143        }
144    }
145}
146
147[export]
148def init() {
149    harness_init("Create by drag", 1000, 600)
150    g_ed = create_node_editor()
151    seed()
152}
153
154[export]
155def update() {
156    if (!harness_begin_frame()) return
157    harness_new_frame()
158    let io & = unsafe(GetIO())
159    SetNextWindowPos(float2(0.0, 0.0), ImGuiCond.Always)
160    SetNextWindowSize(io.DisplaySize, ImGuiCond.Always)
161    let flags = (ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoResize |
162                 ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoScrollbar |
163                 ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoSavedSettings |
164                 ImGuiWindowFlags.NoBringToFrontOnFocus)
165    window(MAIN_WIN, (text = "Create by drag", closable = false, flags = flags)) {
166        draw_editor()
167    }
168    harness_end_frame()
169}
170
171[export]
172def shutdown() {
173    destroy_node_editor(g_ed)
174    harness_shutdown()
175}
176
177[export]
178def main() {
179    init()
180    while (!exit_requested()) {
181        update()
182    }
183    shutdown()
184}

The create scope

begin_create(ed) { ... } is the same scope that commits a hand-dragged link, and it serves both gestures of a pin-drag:

  • query_new_link(ed, a, b) reports the pins when the drag is released on a pin — commit a link, exactly as in connect by drag.

  • show_new_node_drag(ed, from_pin, drop_pos) is true the frame the drag is released in empty canvas. It hands back the source pin and the canvas-space drop point, and is an event (one frame), not a scope — open the create-node UI in response.

The popup is plain ImGui, so it lives in a with_suspended island (screen space) just like the context menus.

The auto-connect

A menu pick spawns the node at the drop point and calls enqueue_new_link(ed, source_pin, new_input_pin). That queued link is not added to the graph directly — it replays through begin_create’s query_new_link on the next frame, the same path a mouse-dragged link takes, so commit_link stores it once. The app gets one code path for “a link appeared”, whether the user dragged it or the editor created it.

Driving it from a test

The recording is a real synthetic pin-drag (see tests/integration/record_create_by_drag.das): press on the output pin, travel to an empty point, release. Because the tutorial’s pins render a real screen-space bbox, the drag targets the pin center directly — a genuine gesture, not an injected one. (shader_graph’s pins have no queryable bbox, so its test_new_node_drag reaches for the ne_new_node_drag injection rail instead.) The menu pick is then an ordinary click resolved from the item’s bbox.

set_user_control(false) hands IO to the synthetic timeline so the real OS cursor can’t race the synth and eat the drag or the menu click.