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

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.