Skip to main content

jellyflow_runtime/runtime/selection/
node_drag_start.rs

1use serde::{Deserialize, Serialize};
2
3use crate::io::{NodeGraphInteractionState, NodeGraphViewState};
4use crate::runtime::drag::{
5    PointerGestureClaim, PointerGestureClaimInput, resolve_pointer_gesture_claim,
6};
7use crate::runtime::policy::resolve_node_interaction_policy;
8use crate::runtime::store::NodeGraphStore;
9use jellyflow_core::core::CanvasPoint;
10use jellyflow_core::core::{EdgeId, Graph, GroupId, NodeId};
11
12use super::types::SelectionModifier;
13
14/// Input for resolving the selection side-effect of starting a node drag.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub struct NodeDragStartSelectionInput {
17    pub node: NodeId,
18    pub modifier: SelectionModifier,
19}
20
21impl NodeDragStartSelectionInput {
22    pub fn new(node: NodeId, multi_selection_active: bool) -> Self {
23        Self {
24            node,
25            modifier: if multi_selection_active {
26                SelectionModifier::Additive
27            } else {
28                SelectionModifier::Replace
29            },
30        }
31    }
32
33    pub fn with_modifier(node: NodeId, modifier: SelectionModifier) -> Self {
34        Self { node, modifier }
35    }
36}
37
38/// Selection mutation implied by starting a node drag.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum NodeDragStartSelectionAction {
41    /// Keep the current selection unchanged.
42    Unchanged,
43    /// Clear node, edge, and group selection.
44    Clear,
45    /// Select only the dragged node and clear edge/group selection.
46    SelectOnly(NodeId),
47    /// Add the dragged node to the existing node selection.
48    Add(NodeId),
49    /// Remove the dragged node from the existing node selection.
50    Remove(NodeId),
51}
52
53/// Combined decision for a node pointer-down that may update selection and enable node dragging.
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub struct NodePointerDownDecision {
56    pub selection: NodeDragStartSelectionAction,
57    pub drag_claim: PointerGestureClaim,
58}
59
60impl NodePointerDownDecision {
61    pub fn new(selection: NodeDragStartSelectionAction, drag_claim: PointerGestureClaim) -> Self {
62        Self {
63            selection,
64            drag_claim,
65        }
66    }
67
68    pub fn apply_to_view_state(self, view_state: &mut NodeGraphViewState) {
69        self.selection.apply_to_view_state(view_state);
70    }
71
72    fn selection_after(
73        self,
74        view_state: &NodeGraphViewState,
75    ) -> Option<(Vec<NodeId>, Vec<EdgeId>, Vec<GroupId>)> {
76        self.selection.selection_after(view_state)
77    }
78}
79
80/// Input for resolving the first node pointer-down decision.
81#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
82pub struct NodePointerDownInput {
83    pub node: NodeId,
84    #[serde(default)]
85    pub multi_selection_active: bool,
86    pub screen_delta: CanvasPoint,
87}
88
89impl NodePointerDownInput {
90    pub fn new(node: NodeId, multi_selection_active: bool, screen_delta: CanvasPoint) -> Self {
91        Self {
92            node,
93            multi_selection_active,
94            screen_delta,
95        }
96    }
97}
98
99impl NodeDragStartSelectionAction {
100    pub fn is_unchanged(self) -> bool {
101        self == Self::Unchanged
102    }
103
104    pub fn apply_to_view_state(self, view_state: &mut NodeGraphViewState) {
105        match self {
106            Self::Unchanged => {}
107            Self::Clear => view_state.set_selection(Vec::new(), Vec::new(), Vec::new()),
108            Self::SelectOnly(node) => view_state.set_selection(vec![node], Vec::new(), Vec::new()),
109            Self::Add(node) => {
110                if !view_state.selected_nodes.contains(&node) {
111                    view_state.selected_nodes.push(node);
112                    view_state.selected_nodes.sort();
113                    view_state.selected_nodes.dedup();
114                }
115            }
116            Self::Remove(node) => {
117                view_state
118                    .selected_nodes
119                    .retain(|selected| *selected != node);
120            }
121        }
122    }
123
124    fn selection_after(
125        self,
126        view_state: &NodeGraphViewState,
127    ) -> Option<(Vec<NodeId>, Vec<EdgeId>, Vec<GroupId>)> {
128        if self.is_unchanged() {
129            return None;
130        }
131
132        let mut next = view_state.clone();
133        self.apply_to_view_state(&mut next);
134        Some((
135            next.selected_nodes,
136            next.selected_edges,
137            next.selected_groups,
138        ))
139    }
140}
141
142/// Resolves XyFlow-compatible selection behavior for a node-drag start.
143///
144/// This mirrors the `selectNodesOnDrag` branch in XyFlow: selectable nodes select on drag start by
145/// default, multi-selection toggles selected nodes, and disabled `selectNodesOnDrag` clears an
146/// existing selection only when dragging an unselected node outside multi-selection mode.
147pub fn resolve_node_drag_start_selection(
148    graph: &Graph,
149    view_state: &NodeGraphViewState,
150    interaction: &NodeGraphInteractionState,
151    input: NodeDragStartSelectionInput,
152) -> NodeDragStartSelectionAction {
153    let Some(node) = graph.nodes.get(&input.node) else {
154        return NodeDragStartSelectionAction::Unchanged;
155    };
156    if node.hidden {
157        return NodeDragStartSelectionAction::Unchanged;
158    }
159
160    let selected = view_state.selected_nodes.contains(&input.node);
161    let selectable = resolve_node_interaction_policy(node, interaction).selectable;
162    let selection = interaction.selection_interaction();
163
164    if (!selection.select_nodes_on_drag || !selectable) && !input.modifier.additive() {
165        return if selected {
166            NodeDragStartSelectionAction::Unchanged
167        } else {
168            NodeDragStartSelectionAction::Clear
169        };
170    }
171
172    if !selectable || !selection.select_nodes_on_drag {
173        return NodeDragStartSelectionAction::Unchanged;
174    }
175
176    if !selected {
177        if input.modifier.additive() {
178            NodeDragStartSelectionAction::Add(input.node)
179        } else {
180            NodeDragStartSelectionAction::SelectOnly(input.node)
181        }
182    } else if input.modifier.additive() {
183        NodeDragStartSelectionAction::Remove(input.node)
184    } else {
185        NodeDragStartSelectionAction::Unchanged
186    }
187}
188
189/// Resolves the first headless decision for a node pointer-down.
190///
191/// This keeps the existing XyFlow-compatible selection side effect while also exposing whether the
192/// pointer state should proceed toward node dragging or is still unclaimed.
193pub fn resolve_node_pointer_down(
194    graph: &Graph,
195    view_state: &NodeGraphViewState,
196    interaction: &NodeGraphInteractionState,
197    input: NodePointerDownInput,
198) -> NodePointerDownDecision {
199    let selection = resolve_node_drag_start_selection(
200        graph,
201        view_state,
202        interaction,
203        NodeDragStartSelectionInput::new(input.node, input.multi_selection_active),
204    );
205    let drag_claim = resolve_pointer_gesture_claim(PointerGestureClaimInput::new(
206        input.screen_delta,
207        input.multi_selection_active,
208        false,
209        false,
210        interaction.node_drag_interaction().node_drag_threshold,
211        interaction.node_drag_interaction().node_drag_threshold,
212    ));
213
214    NodePointerDownDecision::new(selection, drag_claim)
215}
216
217impl NodeGraphStore {
218    /// Applies the selection portion of a node pointer-down decision.
219    pub fn apply_node_pointer_down(
220        &mut self,
221        input: NodePointerDownInput,
222    ) -> NodePointerDownDecision {
223        let interaction = self.resolved_interaction_state();
224        let decision =
225            resolve_node_pointer_down(self.graph(), self.view_state(), &interaction, input);
226        if let Some((nodes, edges, groups)) = decision.selection_after(self.view_state()) {
227            self.set_selection(nodes, edges, groups);
228        }
229        decision
230    }
231
232    /// Applies XyFlow-compatible selection behavior for a node-drag start.
233    pub fn apply_node_drag_start_selection(
234        &mut self,
235        input: NodeDragStartSelectionInput,
236    ) -> NodeDragStartSelectionAction {
237        self.apply_node_pointer_down(NodePointerDownInput::new(
238            input.node,
239            input.modifier.additive(),
240            CanvasPoint::default(),
241        ))
242        .selection
243    }
244
245    /// Resolves the node pointer-down decision against current graph, selection, and interaction.
246    pub fn resolve_node_pointer_down(
247        &self,
248        input: NodePointerDownInput,
249    ) -> NodePointerDownDecision {
250        let interaction = self.resolved_interaction_state();
251        resolve_node_pointer_down(self.graph(), self.view_state(), &interaction, input)
252    }
253}