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_node confirm 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).