Deleting & selection
Selection is the editor’s job — click a node or link and it highlights. Deleting
the selection runs through a begin_delete scope: the editor offers each item
being removed, accept_deleted_link / accept_deleted_node confirm it, and
deleting a node cascades to the links touching its pins.
begin_delete(g_ed) {
var lid = 0
while (query_deleted_link(g_ed, lid)) { // each link the editor is deleting
if (accept_deleted_link(g_ed)) {
g_links |> erase(lid)
}
}
var nid = 0
while (query_deleted_node(g_ed, nid)) { // each node being deleted
if (accept_deleted_node(g_ed)) {
remove_node(nid) // app drops the node + its dangling links
}
}
}
The graph is a tiny A -> B -> C chain (two links). Deleting the middle node B
removes both links, because the editor reports only the node id — the app cascades
to the links on its pins itself (remove_node below).
Source: examples/tutorial/delete_and_select.das.
Walkthrough
The recording is voiced and self-verifying: it pulses both links with flow()
to show the chain is live, then a real synthetic click MUST select B and a
real Delete key MUST remove B and cascade both links on its pins (a no-op
aborts at teardown).
1options gen2
2
3require imgui/imgui_harness
4require imgui/imgui_node_editor_boost_v2
5require imgui/imgui_node_editor_live
6
7// =============================================================================
8// TUTORIAL: delete_and_select — selecting items and the delete cascade.
9//
10// begin_delete(ed) { -- open the delete-interaction scope
11// while (query_deleted_link(ed, lid)) { -- the editor offers each link being deleted
12// if (accept_deleted_link(ed)) ... -- confirm: drop it from your model
13// }
14// while (query_deleted_node(ed, nid)) { -- ...then each deleted node
15// if (accept_deleted_node(ed)) ... -- confirm: drop the node AND its dangling links
16// }
17// }
18//
19// Selection is the editor's job: click a node (or link) and it highlights. Press
20// Delete and the editor raises the selection through begin_delete next frame — we
21// accept each item and drop it from our model. Deleting a NODE must cascade to the
22// links touching its pins: the editor reports only the node id, so the app drops the
23// dangling links itself (remove_node). A->B->C: deleting B takes both links with it.
24// =============================================================================
25
26struct Nd {
27 id : int
28 title : string
29 in_pin : int // 0 when the node has no input pin
30 out_pin : int // 0 when the node has no output pin
31 pos : float2
32}
33
34struct Lk {
35 id : int
36 from_pin : int
37 to_pin : int
38}
39
40var g_nodes : table<int; Nd>
41var g_links : table<int; Lk>
42var g_ed : imgui_node_editor::EditorContext? = null
43var g_seeded : bool = false
44
45// Per-id text slot. A plain text("foo") shares ONE state global per source line, so a
46// loop-rendered title collapses every node's snapshot value to the last drawn. Indexed
47// text(TABLE[id], (text = …)) gives each id its own slot (the data-driven node idiom).
48var NODE_TITLE : table<int; NarrativeState>
49
50def seed() {
51 g_nodes[1] = Nd(id = 1, title = "A", in_pin = 0, out_pin = 11, pos = float2(80.0, 170.0))
52 g_nodes[2] = Nd(id = 2, title = "B", in_pin = 21, out_pin = 22, pos = float2(340.0, 170.0))
53 g_nodes[3] = Nd(id = 3, title = "C", in_pin = 31, out_pin = 0, pos = float2(600.0, 170.0))
54 g_links[100] = Lk(id = 100, from_pin = 11, to_pin = 21)
55 g_links[101] = Lk(id = 101, from_pin = 22, to_pin = 31)
56}
57
58def remove_node(nid : int) {
59 // The editor reports the deleted node by id only — cascade to the links on its
60 // pins ourselves. Snapshot the link keys first: erasing while iterating keys()
61 // would trip the table's iterator lock.
62 return if (!key_exists(g_nodes, nid))
63 let n = g_nodes[nid]
64 var dead <- [for (lk in keys(g_links)); lk]
65 for (lk in dead) {
66 let l = g_links[lk]
67 if (l.from_pin == n.in_pin || l.from_pin == n.out_pin ||
68 l.to_pin == n.in_pin || l.to_pin == n.out_pin) {
69 g_links |> erase(lk)
70 }
71 }
72 delete dead
73 g_nodes |> erase(nid)
74}
75
76def draw_editor() {
77 node_editor("graph", (editor = g_ed)) {
78 if (!g_seeded) {
79 for (n in values(g_nodes)) {
80 imgui_node_editor::SetNodePosition(n.id, n.pos)
81 }
82 g_seeded = true
83 }
84 for (n in values(g_nodes)) {
85 node(n.id) {
86 text(NODE_TITLE[n.id], (text = n.title))
87 if (n.in_pin != 0) {
88 pin(n.in_pin, PinKind.Input) {
89 text("-> in")
90 }
91 }
92 if (n.out_pin != 0) {
93 pin(n.out_pin, PinKind.Output) {
94 text("out ->")
95 }
96 }
97 }
98 }
99 for (l in values(g_links)) {
100 link(l.id, l.from_pin, l.to_pin)
101 }
102 // Processes the deletes the editor raises (e.g. the Delete key on the
103 // selection). Links first, then nodes; a deleted node cascades via remove_node.
104 begin_delete(g_ed) {
105 var lid = 0
106 while (query_deleted_link(g_ed, lid)) {
107 if (accept_deleted_link(g_ed)) {
108 g_links |> erase(lid)
109 }
110 }
111 var nid = 0
112 while (query_deleted_node(g_ed, nid)) {
113 if (accept_deleted_node(g_ed)) {
114 remove_node(nid)
115 }
116 }
117 }
118 }
119}
120
121[export]
122def init() {
123 harness_init("Delete and select", 1000, 600)
124 g_ed = create_node_editor()
125 seed()
126}
127
128[export]
129def update() {
130 if (!harness_begin_frame()) return
131 harness_new_frame()
132 let io & = unsafe(GetIO())
133 SetNextWindowPos(float2(0.0, 0.0), ImGuiCond.Always)
134 SetNextWindowSize(io.DisplaySize, ImGuiCond.Always)
135 let flags = (ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoResize |
136 ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoScrollbar |
137 ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoSavedSettings |
138 ImGuiWindowFlags.NoBringToFrontOnFocus)
139 window(MAIN_WIN, (text = "Delete and select", closable = false, flags = flags)) {
140 draw_editor()
141 }
142 harness_end_frame()
143}
144
145[export]
146def shutdown() {
147 destroy_node_editor(g_ed)
148 harness_shutdown()
149}
150
151[export]
152def main() {
153 init()
154 while (!exit_requested()) {
155 update()
156 }
157 shutdown()
158}
The delete scope
begin_delete(ed) { ... } opens the editor’s delete-interaction scope for the
frame. Inside it:
query_deleted_link(ed, lid)/query_deleted_node(ed, nid)loop over every item the editor wants to delete this frame, writing each id out.accept_deleted_link/accept_deleted_nodeconfirm the removal — the app then drops the item from its own model. (reject_deleted_*veto it instead.)
The cascade
A link references two pins; a node owns its pins. When a node is deleted the
editor reports only the node id, so the app must remove the links whose
endpoints belong to that node — otherwise they dangle. remove_node snapshots
the link keys first (erasing while iterating keys() would trip the table’s
iterator lock), then erases any link touching the node’s input or output pin.
Driving the delete
The editor deletes the current selection on the Delete key, routing each removed
item into begin_delete natively — no app wiring beyond the accept loop above. The
recording selects B with a click, then presses Delete; the editor raises B
(and the links on its pins) through begin_delete and the app accepts each one.
set_user_control(false) hands IO to the synthetic timeline so the canvas gains
hover and the Delete key’s input gate opens.
For programmatic deletes — a toolbar button, a script step — the same path is reachable
through enqueue_delete_node / enqueue_delete_link, which the editor replays
through begin_delete next frame; that enqueue rail is how the test layer deletes
deterministically (ne_delete_node / ne_delete_link).