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_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).