Skip to main content

jellyflow_runtime/runtime/connection/
target.rs

1use serde::{Deserialize, Serialize};
2
3use jellyflow_core::core::{CanvasPoint, CanvasRect, PortDirection};
4use jellyflow_core::interaction::NodeGraphConnectionMode;
5
6use crate::runtime::geometry::HandleBounds;
7
8use super::{
9    ClosestConnectionHandleInput, ConnectionHandleCandidate, ConnectionHandleRef,
10    ConnectionHandleValidity, closest_connection_handle, connection_handle_validity,
11};
12
13/// Candidate target handle plus adapter-resolved connectability policy.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
15pub struct ConnectionTargetHandle {
16    pub handle: ConnectionHandleRef,
17    pub connectable: bool,
18    pub connectable_end: bool,
19}
20
21impl ConnectionTargetHandle {
22    pub fn new(handle: ConnectionHandleRef, connectable: bool, connectable_end: bool) -> Self {
23        Self {
24            handle,
25            connectable,
26            connectable_end,
27        }
28    }
29}
30
31/// Renderer-normalized target-handle geometry plus target connectability policy.
32#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
33pub struct ConnectionTargetCandidate {
34    pub target: ConnectionTargetHandle,
35    pub node_rect: CanvasRect,
36    pub bounds: HandleBounds,
37}
38
39impl ConnectionTargetCandidate {
40    pub fn new(
41        target: ConnectionTargetHandle,
42        node_rect: CanvasRect,
43        bounds: HandleBounds,
44    ) -> Self {
45        Self {
46            target,
47            node_rect,
48            bounds,
49        }
50    }
51
52    fn handle_candidate(self) -> ConnectionHandleCandidate {
53        ConnectionHandleCandidate::new(self.target.handle, self.node_rect, self.bounds)
54    }
55}
56
57/// XyFlow-shaped connection endpoints resolved from a start handle and target handle.
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
59pub struct ConnectionHandleConnection {
60    pub source: ConnectionHandleRef,
61    pub target: ConnectionHandleRef,
62}
63
64impl ConnectionHandleConnection {
65    pub fn from_start_and_target(from: ConnectionHandleRef, target: ConnectionHandleRef) -> Self {
66        if from.direction == PortDirection::In {
67            Self {
68                source: target,
69                target: from,
70            }
71        } else {
72            Self {
73                source: from,
74                target,
75            }
76        }
77    }
78}
79
80/// Input for resolving whether a target handle can complete a connection gesture.
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
82pub struct ConnectionTargetInput {
83    pub from: ConnectionHandleRef,
84    #[serde(default, skip_serializing_if = "Option::is_none")]
85    pub target: Option<ConnectionTargetHandle>,
86    pub mode: NodeGraphConnectionMode,
87    #[serde(default)]
88    pub is_inside_connection_radius: bool,
89    #[serde(default = "default_connection_validity")]
90    pub is_valid_connection: bool,
91}
92
93impl ConnectionTargetInput {
94    pub fn new(
95        from: ConnectionHandleRef,
96        target: Option<ConnectionTargetHandle>,
97        mode: NodeGraphConnectionMode,
98        is_inside_connection_radius: bool,
99    ) -> Self {
100        Self {
101            from,
102            target,
103            mode,
104            is_inside_connection_radius,
105            is_valid_connection: true,
106        }
107    }
108
109    pub fn with_connection_validity(mut self, is_valid_connection: bool) -> Self {
110        self.is_valid_connection = is_valid_connection;
111        self
112    }
113}
114
115/// Input for resolving the current connection target from renderer-normalized handle candidates.
116#[derive(Debug, Clone, Copy, PartialEq)]
117pub struct ConnectionTargetFromHandlesInput<'a> {
118    pub pointer: CanvasPoint,
119    pub radius: f32,
120    pub from: ConnectionHandleRef,
121    pub candidates: &'a [ConnectionTargetCandidate],
122    pub mode: NodeGraphConnectionMode,
123    pub is_valid_connection: bool,
124}
125
126impl<'a> ConnectionTargetFromHandlesInput<'a> {
127    pub fn new(
128        pointer: CanvasPoint,
129        radius: f32,
130        from: ConnectionHandleRef,
131        candidates: &'a [ConnectionTargetCandidate],
132        mode: NodeGraphConnectionMode,
133    ) -> Self {
134        Self {
135            pointer,
136            radius,
137            from,
138            candidates,
139            mode,
140            is_valid_connection: true,
141        }
142    }
143
144    pub fn with_connection_validity(mut self, is_valid_connection: bool) -> Self {
145        self.is_valid_connection = is_valid_connection;
146        self
147    }
148}
149
150fn default_connection_validity() -> bool {
151    true
152}
153
154/// Resolved target semantics for connection feedback and completion callbacks.
155#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
156pub struct ResolvedConnectionTarget {
157    pub target: Option<ConnectionTargetHandle>,
158    pub connection: Option<ConnectionHandleConnection>,
159    pub is_handle_valid: bool,
160    pub feedback: ConnectionHandleValidity,
161}
162
163/// Resolves XyFlow handle target semantics without depending on DOM hit testing.
164///
165/// Adapters decide which handle is under or near the pointer. This function owns the shared
166/// source/target ordering, strict/loose mode checks, target connectability, and feedback state.
167pub fn resolve_connection_target(input: ConnectionTargetInput) -> ResolvedConnectionTarget {
168    let Some(target) = input.target else {
169        return ResolvedConnectionTarget {
170            target: None,
171            connection: None,
172            is_handle_valid: false,
173            feedback: connection_handle_validity(input.is_inside_connection_radius, false),
174        };
175    };
176
177    let connection = ConnectionHandleConnection::from_start_and_target(input.from, target.handle);
178    let mode_allows_target = match input.mode {
179        NodeGraphConnectionMode::Strict => input.from.direction != target.handle.direction,
180        NodeGraphConnectionMode::Loose => {
181            input.from.node != target.handle.node || input.from.port != target.handle.port
182        }
183    };
184    let is_handle_valid = target.connectable
185        && target.connectable_end
186        && mode_allows_target
187        && input.is_valid_connection;
188
189    ResolvedConnectionTarget {
190        target: Some(target),
191        connection: Some(connection),
192        is_handle_valid,
193        feedback: connection_handle_validity(input.is_inside_connection_radius, is_handle_valid),
194    }
195}
196
197/// Resolves connection target feedback from renderer-normalized handle geometry and policies.
198///
199/// Adapters still own DOM/window hit testing and provide the candidate handle inventory. The runtime
200/// owns XyFlow-compatible closest-handle tie semantics, strict/loose checks, endpoint ordering, and
201/// validity feedback.
202pub fn resolve_connection_target_from_handles(
203    input: ConnectionTargetFromHandlesInput<'_>,
204) -> ResolvedConnectionTarget {
205    let handle_candidates = input
206        .candidates
207        .iter()
208        .copied()
209        .map(ConnectionTargetCandidate::handle_candidate)
210        .collect::<Vec<_>>();
211    let closest = closest_connection_handle(ClosestConnectionHandleInput::new(
212        input.pointer,
213        input.radius,
214        input.from,
215        &handle_candidates,
216    ));
217    let target = closest.and_then(|closest| {
218        input
219            .candidates
220            .iter()
221            .find(|candidate| candidate.target.handle == closest.handle)
222            .map(|candidate| candidate.target)
223    });
224
225    resolve_connection_target(
226        ConnectionTargetInput::new(input.from, target, input.mode, closest.is_some())
227            .with_connection_validity(input.is_valid_connection),
228    )
229}