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.