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.