Clipboard

Duplicate, copy, cut and paste are the editor’s edit shortcuts — Ctrl+D / Ctrl+C / Ctrl+X / Ctrl+V. imgui-node-editor owns no clipboard content: it only reports that a chord fired, through a with_shortcuts scope. The app owns the clipboard — here each copied node’s title plus its position relative to the cluster top-left — and recreates it on paste.

with_shortcuts(g_ed) {                       // INSIDE node_editor, not with_suspended
    if (accept_copy(g_ed)) {
        g_action = "copy"
    } elif (accept_paste(g_ed)) {
        g_action = "paste"
        g_paste_anchor = ...                 // capture the cursor in canvas space now
    } elif (accept_duplicate(g_ed)) {
        g_action = "duplicate"
    }
}
// ... after the node_editor block:
handle_action()                              // run the copy/paste where the helpers are safe

Source: examples/tutorial/clipboard.das.

Walkthrough

  1options gen2
  2
  3require imgui/imgui_harness
  4require imgui/imgui_node_editor_boost_v2
  5require imgui/imgui_node_editor_live
  6
  7// =============================================================================
  8// TUTORIAL: clipboard — duplicate / copy / paste via the editor's edit shortcuts.
  9//
 10//   with_shortcuts(ed) {                       -- INSIDE node_editor, NOT with_suspended
 11//       if (accept_copy(ed))      { ... }      -- Ctrl+C fired this frame
 12//       elif (accept_paste(ed))   { ... }      -- Ctrl+V
 13//       elif (accept_duplicate(ed)){ ... }     -- Ctrl+D
 14//   }
 15//
 16// imgui-node-editor owns NO clipboard content — accept_* only report that the chord
 17// fired. The APP owns the clipboard: copy serializes the selected cluster (here: each
 18// node's title + position relative to the cluster top-left), paste recreates it. accept_*
 19// runs while the editor is current, so it just FLAGS the action; the actual copy/paste
 20// runs after the node_editor block (handle_action), where the bracketed selection/spawn
 21// helpers are safe — the same flag-then-act split the other tutorials use.
 22//
 23// Shortcuts are Ctrl+C / Ctrl+V / Ctrl+D on every platform (the editor checks io.KeyCtrl
 24// directly, so they are NOT remapped to Cmd on macOS) and the editor must be focused —
 25// clicking a node both selects it and focuses the canvas.
 26//
 27// STANDALONE: daslang.exe modules/dasImguiNodeEditor/examples/tutorial/clipboard.das
 28// LIVE:       daslang-live modules/dasImguiNodeEditor/examples/tutorial/clipboard.das
 29// =============================================================================
 30
 31struct Nd {
 32    id      : int
 33    title   : string
 34    out_pin : int
 35    pos     : float2
 36}
 37
 38var g_nodes   : table<int; Nd>
 39var g_ed      : imgui_node_editor::EditorContext? = null
 40var g_seeded  : bool = false
 41var g_next_id : int = 200          // ids for clipboard-created nodes (out pin = id+1)
 42var g_action  : string = ""        // shortcut accepted this frame; handled AFTER the editor block
 43var g_paste_anchor : float2        // canvas pos captured at accept_paste (where paste lands)
 44
 45// App-owned clipboard: each copied node's title + position relative to the cluster
 46// top-left (g_clip_origin). Paste recreates the cluster, preserving relative layout.
 47struct ClipNode {
 48    title : string
 49    rel   : float2
 50}
 51var g_clip        : array<ClipNode>
 52var g_clip_origin : float2
 53
 54var NODE_TITLE : table<int; NarrativeState>   // per-id title slot (data-driven node idiom)
 55
 56def seed() {
 57    g_nodes[1] = Nd(id = 1, title = "A", out_pin = 11, pos = float2(180.0, 170.0))
 58    g_nodes[2] = Nd(id = 2, title = "B", out_pin = 21, pos = float2(180.0, 360.0))
 59}
 60
 61def spawn(title : string; pos : float2) : int {
 62    let nid = g_next_id
 63    g_next_id += 10
 64    g_nodes[nid] = Nd(id = nid, title = title, out_pin = nid + 1, pos = pos)
 65    set_node_position(g_ed, nid, pos)   // runs post-editor-block -> bracketed wrapper
 66    return nid
 67}
 68
 69// ===== app-owned clipboard =====
 70
 71def clipboard_copy() {
 72    var sel <- get_selected_nodes(g_ed)
 73    g_clip |> clear()
 74    var origin = float2(1.0e9, 1.0e9)
 75    var picked : array<int>
 76    picked |> reserve(length(sel))
 77    for (nid in sel) {
 78        continue if (!key_exists(g_nodes, nid))
 79        let p = get_node_position(g_ed, nid)
 80        picked |> push(nid)
 81        if (p.x < origin.x) {
 82            origin.x = p.x
 83        }
 84        if (p.y < origin.y) {
 85            origin.y = p.y
 86        }
 87    }
 88    g_clip_origin = empty(picked) ? float2(0.0, 0.0) : origin
 89    g_clip |> reserve(length(picked))
 90    for (nid in picked) {
 91        let p = get_node_position(g_ed, nid)
 92        g_clip |> push(ClipNode(title = g_nodes[nid].title, rel = p - g_clip_origin))
 93    }
 94    delete picked
 95    delete sel
 96}
 97
 98def clipboard_paste(anchor : float2) {
 99    return if (empty(g_clip))
100    clear_selection(g_ed)
101    for (ce in g_clip) {
102        let nid = spawn(ce.title, anchor + ce.rel)
103        select_node(g_ed, nid, true)    // pasted nodes come in selected (chains a follow-up paste)
104    }
105}
106
107def handle_action() {
108    if (g_action == "copy") {
109        clipboard_copy()
110    } elif (g_action == "paste") {
111        clipboard_paste(g_paste_anchor)
112    } elif (g_action == "duplicate") {
113        clipboard_copy()
114        clipboard_paste(g_clip_origin + float2(40.0, 40.0))    // near the originals, layout preserved
115    }
116    g_action = ""
117}
118
119def draw_editor() {
120    node_editor("graph", (editor = g_ed)) {
121        if (!g_seeded) {
122            for (n in values(g_nodes)) {
123                imgui_node_editor::SetNodePosition(n.id, n.pos)
124            }
125            g_seeded = true
126        }
127        for (n in values(g_nodes)) {
128            node(n.id) {
129                text(NODE_TITLE[n.id], (text = n.title))
130                pin(n.out_pin, PinKind.Output) {
131                    text("out ->")
132                }
133            }
134        }
135        // Ctrl+C / Ctrl+V / Ctrl+D. accept_* only flags the action (editor is current);
136        // handle_action runs the copy/paste post-block where the bracketed helpers are safe.
137        with_shortcuts(g_ed) {
138            if (accept_copy(g_ed)) {
139                g_action = "copy"
140            } elif (accept_paste(g_ed)) {
141                g_action = "paste"
142                // Capture the cursor in canvas space now (editor current). A keyboard paste
143                // with no on-canvas cursor (and every headless frame) reads -FLT_MAX, so fall
144                // back to just past the copied cluster — the offset duplicate uses.
145                g_paste_anchor = (is_mouse_pos_valid()
146                    ? imgui_node_editor::ScreenToCanvas(GetMousePos())
147                    : g_clip_origin + float2(40.0, 40.0))
148            } elif (accept_duplicate(g_ed)) {
149                g_action = "duplicate"
150            }
151        }
152    }
153    handle_action()
154}
155
156[export]
157def init() {
158    harness_init("Clipboard", 1000, 600)
159    g_ed = create_node_editor()
160    seed()
161}
162
163[export]
164def update() {
165    if (!harness_begin_frame()) return
166    harness_new_frame()
167    let io & = unsafe(GetIO())
168    SetNextWindowPos(float2(0.0, 0.0), ImGuiCond.Always)
169    SetNextWindowSize(io.DisplaySize, ImGuiCond.Always)
170    let flags = (ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoResize |
171                 ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoScrollbar |
172                 ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoSavedSettings |
173                 ImGuiWindowFlags.NoBringToFrontOnFocus)
174    window(MAIN_WIN, (text = "Clipboard", closable = false, flags = flags)) {
175        draw_editor()
176    }
177    harness_end_frame()
178}
179
180[export]
181def shutdown() {
182    destroy_node_editor(g_ed)
183    harness_shutdown()
184}
185
186[export]
187def main() {
188    init()
189    while (!exit_requested()) {
190        update()
191    }
192    shutdown()
193}

The app owns the clipboard

with_shortcuts(ed) { ... } brackets the editor’s shortcut scope; inside it, accept_copy / accept_cut / accept_paste / accept_duplicate each return true the frame their chord fires. The editor stores nothing — accept_copy is just “a Copy happened”, and the app responds by serializing its selection. This tutorial’s clipboard is deliberately small (node titles + relative positions); shader_graph.das shows the full version that also captures the links internal to the selection and remaps them onto the pasted pins.

Flag now, act later

accept_* runs while the editor is current, so it only flags the action (g_action). The real work — get_selected_nodes, set_node_position, select_node — runs in handle_action after the node_editor block, where those bracketed helpers are safe to call. This is the same flag-then-act split the other tutorials use. Paste captures the cursor anchor at accept_paste (the editor is current, so ScreenToCanvas works), falling back to a fixed offset when there is no on-canvas cursor — which is every headless frame.

Driving it from a test

The shortcuts are Ctrl on every platform: imgui-node-editor checks io.KeyCtrl directly, so they are not remapped to Cmd on macOS the way an ImGui text field is. The gate is that the editor must be focused — a click into the canvas focuses it. So the recording (see tests/integration/record_clipboard.das) clicks a node — which both selects it and focuses the canvas — then sends a real Ctrl chord:

post_command(app, "imgui_key_chord", JV((mods = ["Ctrl"], key = "D")))

The recording app holds set_user_control(false) for the whole run, so the real OS cursor can’t race the synth and steal the canvas focus the chord needs. It also overlays the imgui_key_hud keycap strip, so each Ctrl chord is visible on screen as it fires. The headless regression (test_clipboard_tutorial.das) drives the same real chords — distinct from test_shortcuts / test_clipboard, which exercise shader_graph through the ne_shortcut injection rail.