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
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: delete_and_select — selecting items and the delete cascade.
10//
11// begin_delete(ed) { -- open the delete-interaction scope
12// while (query_deleted_link(ed, lid)) { -- the editor offers each link being deleted
13// if (accept_deleted_link(ed)) ... -- confirm: drop it from your model
14// }
15// while (query_deleted_node(ed, nid)) { -- ...then each deleted node
16// if (accept_deleted_node(ed)) ... -- confirm: drop the node AND its dangling links
17// }
18// }
19//
20// Selection is the editor's job: click a node (or link) and it highlights. Press
21// Delete and the editor raises the selection through begin_delete next frame — we
22// accept each item and drop it from our model. Deleting a NODE must cascade to the
23// links touching its pins: the editor reports only the node id, so the app drops the
24// dangling links itself (remove_node). A->B->C: deleting B takes both links with it.
25// =============================================================================
26
27struct Nd {
28 id : int
29 title : string
30 in_pin : int // 0 when the node has no input pin
31 out_pin : int // 0 when the node has no output pin
32 pos : float2
33}
34
35struct Lk {
36 id : int
37 from_pin : int
38 to_pin : int
39}
40
41var g_nodes : table<int; Nd>
42var g_links : table<int; Lk>
43var g_ed : imgui_node_editor::EditorContext? = null
44var g_seeded : bool = false
45
46// Per-id text slot. A plain text("foo") shares ONE state global per source line, so a
47// loop-rendered title collapses every node's snapshot value to the last drawn. Indexed
48// text(TABLE[id], (text = …)) gives each id its own slot (the data-driven node idiom).
49var NODE_TITLE : table<int; NarrativeState>
50
51def seed() {
52 g_nodes[1] = Nd(id = 1, title = "A", in_pin = 0, out_pin = 11, pos = float2(80.0, 170.0))
53 g_nodes[2] = Nd(id = 2, title = "B", in_pin = 21, out_pin = 22, pos = float2(340.0, 170.0))
54 g_nodes[3] = Nd(id = 3, title = "C", in_pin = 31, out_pin = 0, pos = float2(600.0, 170.0))
55 g_links[100] = Lk(id = 100, from_pin = 11, to_pin = 21)
56 g_links[101] = Lk(id = 101, from_pin = 22, to_pin = 31)
57}
58
59def remove_node(nid : int) {
60 // The editor reports the deleted node by id only — cascade to the links on its
61 // pins ourselves. Snapshot the link keys first: erasing while iterating keys()
62 // would trip the table's iterator lock.
63 return if (!key_exists(g_nodes, nid))
64 let n = g_nodes[nid]
65 var dead <- [for (lk in keys(g_links)); lk]
66 for (lk in dead) {
67 let l = g_links[lk]
68 if (l.from_pin == n.in_pin || l.from_pin == n.out_pin ||
69 l.to_pin == n.in_pin || l.to_pin == n.out_pin) {
70 g_links |> erase(lk)
71 }
72 }
73 delete dead
74 g_nodes |> erase(nid)
75}
76
77def draw_editor() {
78 node_editor("graph", (editor = g_ed)) {
79 if (!g_seeded) {
80 for (n in values(g_nodes)) {
81 imgui_node_editor::SetNodePosition(n.id, n.pos)
82 }
83 g_seeded = true
84 }
85 for (n in values(g_nodes)) {
86 node(n.id) {
87 text(NODE_TITLE[n.id], (text = n.title))
88 if (n.in_pin != 0) {
89 pin(n.in_pin, PinKind.Input) {
90 text("-> in")
91 }
92 }
93 if (n.out_pin != 0) {
94 pin(n.out_pin, PinKind.Output) {
95 text("out ->")
96 }
97 }
98 }
99 }
100 for (l in values(g_links)) {
101 link(l.id, l.from_pin, l.to_pin)
102 }
103 // Processes the deletes the editor raises (e.g. the Delete key on the
104 // selection). Links first, then nodes; a deleted node cascades via remove_node.
105 begin_delete(g_ed) {
106 var lid = 0
107 while (query_deleted_link(g_ed, lid)) {
108 if (accept_deleted_link(g_ed)) {
109 g_links |> erase(lid)
110 }
111 }
112 var nid = 0
113 while (query_deleted_node(g_ed, nid)) {
114 if (accept_deleted_node(g_ed)) {
115 remove_node(nid)
116 }
117 }
118 }
119 }
120}
121
122[export]
123def init() {
124 harness_init("Delete and select", 1000, 600)
125 g_ed = create_node_editor()
126 seed()
127}
128
129[export]
130def update() {
131 if (!harness_begin_frame()) return
132 harness_new_frame()
133 let io & = unsafe(GetIO())
134 SetNextWindowPos(float2(0.0, 0.0), ImGuiCond.Always)
135 SetNextWindowSize(io.DisplaySize, ImGuiCond.Always)
136 let flags = (ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoResize |
137 ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoScrollbar |
138 ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoSavedSettings |
139 ImGuiWindowFlags.NoBringToFrontOnFocus)
140 window(MAIN_WIN, (text = "Delete and select", closable = false, flags = flags)) {
141 draw_editor()
142 }
143 harness_end_frame()
144}
145
146[export]
147def shutdown() {
148 destroy_node_editor(g_ed)
149 harness_shutdown()
150}
151
152[export]
153def main() {
154 init()
155 while (!exit_requested()) {
156 update()
157 }
158 shutdown()
159}
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).