Skip to main content

jellyflow_runtime/runtime/connection/
lifecycle.rs

1use serde::{Deserialize, Serialize};
2
3use crate::runtime::events::{ConnectDragKind, ConnectEnd, ConnectEndOutcome, ConnectStart};
4use crate::schema::MenuDescriptor;
5use jellyflow_core::core::{CanvasPoint, PortId};
6use jellyflow_core::interaction::NodeGraphConnectionMode;
7
8use super::{ConnectionHandleConnection, ResolvedConnectionTarget};
9
10/// Adapter-normalized intent for ending a connection gesture.
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12#[serde(tag = "kind", content = "data", rename_all = "snake_case")]
13pub enum ConnectionEndIntent {
14    /// End the gesture at the currently resolved hover target.
15    Complete,
16    /// Explicit cancellation: escape key, lost focus, tool switch, or adapter-level cancel action.
17    Cancel,
18    /// Pointer was released on the pane/background instead of on a handle.
19    DropOnPane {
20        pointer: CanvasPoint,
21        #[serde(default, skip_serializing_if = "Option::is_none")]
22        menu: Option<MenuDescriptor>,
23    },
24}
25
26/// Stable high-level connection lifecycle state for adapter presentation.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
28#[serde(rename_all = "snake_case")]
29pub enum ConnectionLifecycleState {
30    Started,
31    HoverValid,
32    HoverInvalid,
33    HoverEmpty,
34    Committed,
35    Rejected,
36    Canceled,
37    DroppedOnPane,
38    NoOp,
39}
40
41/// Adapter-facing connection lifecycle result.
42#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
43pub struct ConnectionLifecycleResult {
44    pub start: ConnectStart,
45    pub state: ConnectionLifecycleState,
46    #[serde(default, skip_serializing_if = "Option::is_none")]
47    pub hover: Option<ResolvedConnectionTarget>,
48    #[serde(default, skip_serializing_if = "Option::is_none")]
49    pub connection: Option<ConnectionHandleConnection>,
50    #[serde(default, skip_serializing_if = "Option::is_none")]
51    pub target: Option<PortId>,
52    pub end: ConnectEnd,
53    #[serde(default, skip_serializing_if = "Option::is_none")]
54    pub dropped_at: Option<CanvasPoint>,
55    #[serde(default, skip_serializing_if = "Option::is_none")]
56    pub dropped_wire_menu: Option<MenuDescriptor>,
57}
58
59impl ConnectionLifecycleResult {
60    pub fn for_hover(start: ConnectStart, hover: Option<ResolvedConnectionTarget>) -> Self {
61        let state = match hover {
62            Some(hover) if hover.is_handle_valid => ConnectionLifecycleState::HoverValid,
63            Some(hover) if hover.target.is_some() => ConnectionLifecycleState::HoverInvalid,
64            _ => ConnectionLifecycleState::HoverEmpty,
65        };
66
67        Self {
68            end: ConnectEnd {
69                kind: start.kind.clone(),
70                mode: start.mode,
71                target: hover.and_then(|hover| hover.target.map(|target| target.handle.port)),
72                outcome: ConnectEndOutcome::NoOp,
73            },
74            start,
75            state,
76            hover,
77            connection: hover.and_then(|hover| hover.connection),
78            target: hover.and_then(|hover| hover.target.map(|target| target.handle.port)),
79            dropped_at: None,
80            dropped_wire_menu: None,
81        }
82    }
83
84    pub fn did_commit(&self) -> bool {
85        matches!(self.state, ConnectionLifecycleState::Committed)
86    }
87
88    pub fn opens_dropped_wire_menu(&self) -> bool {
89        matches!(self.state, ConnectionLifecycleState::DroppedOnPane)
90    }
91}
92
93/// Resolves a connection gesture lifecycle result from renderer-normalized inputs.
94pub fn resolve_connection_lifecycle(
95    start: ConnectStart,
96    hover: Option<ResolvedConnectionTarget>,
97    intent: ConnectionEndIntent,
98) -> ConnectionLifecycleResult {
99    let (state, connection, target, outcome, dropped_at, dropped_wire_menu) = match intent {
100        ConnectionEndIntent::Cancel => (
101            ConnectionLifecycleState::Canceled,
102            None,
103            None,
104            ConnectEndOutcome::Canceled,
105            None,
106            None,
107        ),
108        ConnectionEndIntent::DropOnPane { pointer, menu } => (
109            ConnectionLifecycleState::DroppedOnPane,
110            None,
111            None,
112            ConnectEndOutcome::OpenInsertNodePicker,
113            Some(pointer),
114            menu,
115        ),
116        ConnectionEndIntent::Complete => resolve_completed_lifecycle(hover),
117    };
118
119    let end = ConnectEnd {
120        kind: start.kind.clone(),
121        mode: start.mode,
122        target,
123        outcome,
124    };
125
126    ConnectionLifecycleResult {
127        start,
128        state,
129        hover,
130        connection,
131        target,
132        end,
133        dropped_at,
134        dropped_wire_menu,
135    }
136}
137
138fn resolve_completed_lifecycle(
139    hover: Option<ResolvedConnectionTarget>,
140) -> (
141    ConnectionLifecycleState,
142    Option<ConnectionHandleConnection>,
143    Option<PortId>,
144    ConnectEndOutcome,
145    Option<CanvasPoint>,
146    Option<MenuDescriptor>,
147) {
148    let Some(hover) = hover else {
149        return (
150            ConnectionLifecycleState::NoOp,
151            None,
152            None,
153            ConnectEndOutcome::NoOp,
154            None,
155            None,
156        );
157    };
158
159    if hover.is_handle_valid {
160        let connection = hover.connection;
161        let target = connection.map(|connection| connection.target.port);
162        return (
163            ConnectionLifecycleState::Committed,
164            connection,
165            target,
166            ConnectEndOutcome::Committed,
167            None,
168            None,
169        );
170    }
171
172    let target = hover.target.map(|target| target.handle.port);
173    let state = if hover.target.is_some() {
174        ConnectionLifecycleState::Rejected
175    } else {
176        ConnectionLifecycleState::NoOp
177    };
178    let outcome = if hover.target.is_some() {
179        ConnectEndOutcome::Rejected
180    } else {
181        ConnectEndOutcome::NoOp
182    };
183
184    (state, hover.connection, target, outcome, None, None)
185}
186
187/// Creates a lifecycle start payload for a new edge gesture.
188pub fn new_connection_start(
189    from: PortId,
190    bundle: impl IntoIterator<Item = PortId>,
191    mode: NodeGraphConnectionMode,
192) -> ConnectStart {
193    ConnectStart {
194        kind: ConnectDragKind::New {
195            from,
196            bundle: bundle.into_iter().collect(),
197        },
198        mode,
199    }
200}