From 3870d6da06fd67f46fed0a590ba4b5a4270ba555 Mon Sep 17 00:00:00 2001 From: Mike Solar Date: Thu, 20 Aug 2026 17:08:25 +0800 Subject: [PATCH] feat(node_graph,effect_stack): programmatic selection and card selection events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NodeGraphView::set_selection selects nodes programmatically (emitting SelectionChanged); EffectStackDataSource::selected_effect highlights the chosen card and EffectStackEvent::CardSelected reports card clicks — the two-way clip/node selection sync between the timeline, node editor and inspector. --- crates/gpui/examples/learn/effect_stack.rs | 1 + crates/gpui/src/effect_stack/data.rs | 9 +++++++++ crates/gpui/src/effect_stack/stack_view.rs | 21 +++++++++++++++++---- crates/gpui/src/node_graph/graph_view.rs | 10 ++++++++++ 4 files changed, 37 insertions(+), 4 deletions(-) diff --git a/crates/gpui/examples/learn/effect_stack.rs b/crates/gpui/examples/learn/effect_stack.rs index 6f78de184e..2e9b7e0043 100644 --- a/crates/gpui/examples/learn/effect_stack.rs +++ b/crates/gpui/examples/learn/effect_stack.rs @@ -198,6 +198,7 @@ impl StackDemoRoot { | EffectStackEvent::RemoveRequested(_) | EffectStackEvent::AddRequested { .. } | EffectStackEvent::ContextMenuRequested { .. } + | EffectStackEvent::CardSelected { .. } | EffectStackEvent::ParameterChanged { .. } => { todo!("apply {event:?} to the mock model (or engine, in a real app)") } diff --git a/crates/gpui/src/effect_stack/data.rs b/crates/gpui/src/effect_stack/data.rs index c584a95e52..4357ce8fa4 100644 --- a/crates/gpui/src/effect_stack/data.rs +++ b/crates/gpui/src/effect_stack/data.rs @@ -162,6 +162,15 @@ pub trait EffectStackDataSource: 'static { /// state instead of cards and suppresses all card interactions. fn target_label(&self) -> Option; + /// The card the stack should highlight as selected — the effect whose + /// node is selected in the node graph, when the two views are linked. + /// `None` means no card is highlighted. Defaults to `None`. + /// + /// This is a read-only selection mirror; the view never mutates it. + fn selected_effect(&self) -> Option { + None + } + /// Whether dropping the given effect at `new_index` (an index into the /// list returned by [`effects`](EffectStackDataSource::effects)) would /// be a valid reorder. diff --git a/crates/gpui/src/effect_stack/stack_view.rs b/crates/gpui/src/effect_stack/stack_view.rs index 68ca6d6ff2..9e3f18e5ab 100644 --- a/crates/gpui/src/effect_stack/stack_view.rs +++ b/crates/gpui/src/effect_stack/stack_view.rs @@ -67,6 +67,14 @@ pub enum EffectStackEvent { /// The desired new expansion state. expanded: bool, }, + /// The user clicked a card header to select it (the same gesture that + /// toggles expansion). Emitted alongside + /// [`ExpansionToggled`](Self::ExpansionToggled) so hosts can sync the + /// effect selection with the node graph (highlighting the effect's node). + CardSelected { + /// The clicked effect. + effect: EffectId, + }, /// The user clicked the remove button on a removable card. RemoveRequested(EffectId), /// The user invoked an "add effect" affordance at a stack position. @@ -274,7 +282,9 @@ impl EffectStackView { } /// Toggles a card's expansion by emitting - /// [`EffectStackEvent::ExpansionToggled`]. + /// [`EffectStackEvent::ExpansionToggled`], and reports the card click as + /// [`EffectStackEvent::CardSelected`] so hosts can sync the node-graph + /// selection with the stack. fn toggle_expanded(&mut self, id: EffectId, cx: &mut Context) { self.drag_state = DragState::default(); let expanded = self @@ -289,6 +299,7 @@ impl EffectStackView { effect: id, expanded, }); + cx.emit(EffectStackEvent::CardSelected { effect: id }); cx.notify(); } @@ -360,9 +371,9 @@ impl Render for EffectStackView { self.drag_state = DragState::default(); } - let (label, effects) = { + let (label, effects, selected_effect) = { let data = self.data.read(cx); - (data.target_label(), data.effects()) + (data.target_label(), data.effects(), data.selected_effect()) }; let insertion_index = self.drag_state.insertion_index; let dragged_id = self.drag_state.dragged; @@ -532,7 +543,9 @@ impl Render for EffectStackView { .w_full() .rounded_md() .border_1() - .border_color(if fixed { + .border_color(if selected_effect == Some(id) { + colors.selected + } else if fixed { colors.border } else { colors.separator diff --git a/crates/gpui/src/node_graph/graph_view.rs b/crates/gpui/src/node_graph/graph_view.rs index eb9d304cfc..e10b89a7b4 100644 --- a/crates/gpui/src/node_graph/graph_view.rs +++ b/crates/gpui/src/node_graph/graph_view.rs @@ -318,6 +318,16 @@ impl NodeGraphView { &mut self.state } + /// Programmatically replaces the selection (e.g. to sync the graph with + /// a timeline selection or an inspector card click). Emits + /// [`NodeGraphEvent::SelectionChanged`] when the set changed, so hosts + /// keep their engine-side selection mirror consistent. Callers should + /// not call `cx.notify()` on the view themselves — this method does. + pub fn set_selection(&mut self, nodes: BTreeSet, cx: &mut Context) { + self.set_selection_and_emit(nodes, cx); + cx.notify(); + } + /// Returns the data-source entity this view renders. pub fn data(&self) -> &Entity { &self.data