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

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.