Skip to content

Selection state changes are not always reported via NodeResponse::changed() #47

@mitchmindtree

Description

@mitchmindtree

Problem Description

When multiple nodes are selected and a user clicks on a new node (without Ctrl), the previously selected nodes are deselected but never report this change via NodeResponse::changed(). This causes downstream systems that rely on changed() to track selection state to become out of sync with egui_graph's internal selection.

Root Cause

The issue occurs in Node::show_impl() (src/node.rs:377-380) when handling node selection:

// Clear other selections if ctrl is not pressed and this is newly pressed.
if !ctrl_down && !was_selected {
    gmem.selection.nodes.clear();  // Clears ALL other selected nodes
}
selection_changed = gmem.selection.nodes.insert(self.id);  // Only tracks THIS node's change

When gmem.selection.nodes.clear() is called:

  1. All other nodes are immediately deselected in the shared GraphTempMemory
  2. Only the clicked node knows it was selected and sets selection_changed = true
  3. Other deselected nodes never report changed() because:
    • Nodes processed before the clicked node already ran and saw themselves as selected
    • Nodes processed after the clicked node see themselves as deselected but have no way to know they were selected at the start of the frame

Proposed Solution

The fundamental issue is that selection is a graph-level concern, but changes are reported at the node level. Since nodes are processed sequentially, they cannot reliably report selection changes that affect other nodes.

Option 1: Introduce GraphResponse (Recommended)

Add a new GraphResponse type returned by Graph::show() that reports graph-level changes:

pub struct GraphResponse {
    /// Nodes that had their selection state change this frame
    pub selection_changed: HashSet<NodeId>,

    /// The current selection (for convenience)
    pub selected_nodes: HashSet<NodeId>,

    /// Nodes that were removed this frame
    pub removed_nodes: HashSet<NodeId>,

    // ... other graph-level events
}

impl Graph {
    pub fn show(self, ui: &mut egui::Ui, view: &mut View) -> GraphResponse {
        // Track selection changes at the graph level
        let previous_selection = /* snapshot at frame start */;

        // ... existing show logic ...

        let current_selection = /* current selection state */;
        let selection_changed = previous_selection.symmetric_difference(&current_selection);

        GraphResponse {
            selection_changed: selection_changed.cloned().collect(),
            selected_nodes: current_selection,
            removed_nodes,
            // ...
        }
    }
}

This would allow applications to handle selection changes reliably:

let graph_response = graph.show(ui, &mut view);

// Update application's selection state
for node_id in &graph_response.selection_changed {
    if graph_response.selected_nodes.contains(node_id) {
        app_state.selection.insert(*node_id);
    } else {
        app_state.selection.remove(node_id);
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions