1use serde::{Deserialize, Serialize};
8
9use crate::runtime::connection::{
10 ConnectEdgeError, ConnectEdgeRequest, ConnectionDragActivationInput, ConnectionEndIntent,
11 ConnectionHandleConnection, ConnectionHandleRef, ConnectionLifecycleResult,
12 ConnectionLifecycleState, ConnectionTargetHandle, ResolvedConnectionTarget,
13 connection_drag_threshold_met, connection_handle_validity, resolve_connection_lifecycle,
14};
15use crate::runtime::drag::{
16 NodeDragRequest, PointerGestureClaim as DragPointerGestureClaim, PointerGestureClaimInput,
17 resolve_pointer_gesture_claim,
18};
19use crate::runtime::events::{
20 ConnectEnd, ConnectEndOutcome, ConnectStart, NodeDragEnd, NodeDragEndOutcome, NodeDragStart,
21 NodeDragUpdate, NodeGraphGestureEvent, ViewportMove, ViewportMoveEnd, ViewportMoveEndOutcome,
22 ViewportMoveKind, ViewportMoveStart,
23};
24use crate::runtime::selection::{
25 SelectionPointerClaim, SelectionPointerClaimInput, resolve_selection_pointer_claim,
26};
27use crate::runtime::store::{DispatchError, DispatchOutcome, NodeGraphStore};
28use crate::runtime::viewport::{
29 ViewportDragPanInput, ViewportGestureContext, ViewportGestureIntent, ViewportGestureRejection,
30 ViewportPointerButton, ViewportTransform, resolve_viewport_drag_pan_gesture,
31};
32use jellyflow_core::core::{CanvasPoint, NodeId};
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(tag = "kind", content = "data", rename_all = "snake_case")]
37pub enum PointerSessionTarget {
38 Node(NodeId),
39 ConnectionHandle(ConnectionHandleRef),
40 Pane { button: ViewportPointerButton },
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
45pub struct PointerSessionClaimInput {
46 pub target: PointerSessionTarget,
47 pub screen_delta: CanvasPoint,
48 pub context: ViewportGestureContext,
49}
50
51impl PointerSessionClaimInput {
52 pub fn new(
53 target: PointerSessionTarget,
54 screen_delta: CanvasPoint,
55 context: ViewportGestureContext,
56 ) -> Self {
57 Self {
58 target,
59 screen_delta,
60 context,
61 }
62 }
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
67#[serde(rename_all = "snake_case")]
68pub enum PointerSessionClaim {
69 None,
70 Selection,
71 Connection,
72 NodeDrag,
73 ViewportPan,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
78#[serde(tag = "kind", content = "data", rename_all = "snake_case")]
79pub enum PointerSessionClaimRejection {
80 TargetUnavailable,
81 TargetPolicyBlocked,
82 ActivationThresholdNotMet,
83 ViewportGesture(ViewportGestureRejection),
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
88pub struct PointerSessionClaimOutcome {
89 pub claim: PointerSessionClaim,
90 #[serde(default, skip_serializing_if = "Option::is_none")]
91 pub rejection: Option<PointerSessionClaimRejection>,
92}
93
94impl PointerSessionClaimOutcome {
95 pub fn claimed(claim: PointerSessionClaim) -> Self {
96 Self {
97 claim,
98 rejection: None,
99 }
100 }
101
102 pub fn rejected(rejection: PointerSessionClaimRejection) -> Self {
103 Self {
104 claim: PointerSessionClaim::None,
105 rejection: Some(rejection),
106 }
107 }
108
109 pub fn is_claimed(self) -> bool {
110 self.claim != PointerSessionClaim::None
111 }
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
116pub struct NodeDragSession {
117 pub node: NodeId,
118 pub start: CanvasPoint,
119 pub to: CanvasPoint,
120}
121
122impl NodeDragSession {
123 pub fn new(node: NodeId, start: CanvasPoint, to: CanvasPoint) -> Self {
124 Self { node, start, to }
125 }
126}
127
128#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
130pub struct ConnectEdgeSession {
131 pub start: ConnectStart,
132 pub request: ConnectEdgeRequest,
133}
134
135impl ConnectEdgeSession {
136 pub fn new(start: ConnectStart, request: ConnectEdgeRequest) -> Self {
137 Self { start, request }
138 }
139}
140
141#[derive(Debug, Clone)]
143pub struct ConnectSessionOutcome {
144 pub lifecycle: ConnectionLifecycleResult,
145 pub end_outcome: ConnectEndOutcome,
146 pub committed_update: Option<DispatchOutcome>,
147}
148
149impl ConnectSessionOutcome {
150 fn committed(lifecycle: ConnectionLifecycleResult, committed_update: DispatchOutcome) -> Self {
151 Self {
152 end_outcome: lifecycle.end.outcome,
153 lifecycle,
154 committed_update: Some(committed_update),
155 }
156 }
157
158 fn without_commit(lifecycle: ConnectionLifecycleResult) -> Self {
159 Self {
160 end_outcome: lifecycle.end.outcome,
161 lifecycle,
162 committed_update: None,
163 }
164 }
165
166 pub fn committed_update(&self) -> Option<&DispatchOutcome> {
167 self.committed_update.as_ref()
168 }
169
170 pub fn lifecycle(&self) -> &ConnectionLifecycleResult {
171 &self.lifecycle
172 }
173}
174
175#[derive(Debug, Clone)]
177pub struct NodeDragSessionOutcome {
178 pub nodes: Vec<NodeId>,
179 pub end_outcome: NodeDragEndOutcome,
180 pub committed_update: Option<DispatchOutcome>,
181}
182
183impl NodeDragSessionOutcome {
184 fn committed(nodes: Vec<NodeId>, committed_update: DispatchOutcome) -> Self {
185 Self {
186 nodes,
187 end_outcome: NodeDragEndOutcome::Committed,
188 committed_update: Some(committed_update),
189 }
190 }
191
192 fn without_commit(nodes: Vec<NodeId>, end_outcome: NodeDragEndOutcome) -> Self {
193 Self {
194 nodes,
195 end_outcome,
196 committed_update: None,
197 }
198 }
199
200 pub fn committed_update(&self) -> Option<&DispatchOutcome> {
201 self.committed_update.as_ref()
202 }
203}
204
205#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
207pub struct ViewportDragPanSession {
208 pub context: ViewportGestureContext,
209 pub input: ViewportDragPanInput,
210}
211
212impl ViewportDragPanSession {
213 pub fn new(context: ViewportGestureContext, input: ViewportDragPanInput) -> Self {
214 Self { context, input }
215 }
216}
217
218#[derive(Debug, Clone, Copy, PartialEq)]
220pub struct ViewportGestureSessionOutcome {
221 pub kind: ViewportMoveKind,
222 pub transform: ViewportTransform,
223}
224
225impl NodeGraphStore {
226 pub fn resolve_pointer_session_claim(
228 &self,
229 input: PointerSessionClaimInput,
230 ) -> PointerSessionClaimOutcome {
231 if input.context.connection_in_progress {
232 return PointerSessionClaimOutcome::claimed(PointerSessionClaim::Connection);
233 }
234 if input.context.user_selection_active {
235 return PointerSessionClaimOutcome::claimed(PointerSessionClaim::Selection);
236 }
237
238 let interaction = self.resolved_interaction_state();
239 let pan = interaction.pan_interaction();
240 let selection_claim = resolve_selection_pointer_claim(SelectionPointerClaimInput::new(
241 input.screen_delta,
242 pan.pane_click_distance,
243 input.context.selection_key_pressed,
244 input.context.user_selection_active,
245 ));
246 if selection_claim != SelectionPointerClaim::Unclaimed {
247 return PointerSessionClaimOutcome::claimed(PointerSessionClaim::Selection);
248 }
249
250 match input.target {
251 PointerSessionTarget::Node(node) => {
252 if let Err(rejection) = self.pointer_target_can_start_node_drag(node) {
253 return PointerSessionClaimOutcome::rejected(rejection);
254 }
255 if pointer_claim_reaches_node_drag(PointerGestureClaimInput::new(
256 input.screen_delta,
257 false,
258 false,
259 false,
260 pan.pane_click_distance,
261 interaction.node_drag_interaction().node_drag_threshold,
262 )) {
263 PointerSessionClaimOutcome::claimed(PointerSessionClaim::NodeDrag)
264 } else {
265 PointerSessionClaimOutcome::rejected(
266 PointerSessionClaimRejection::ActivationThresholdNotMet,
267 )
268 }
269 }
270 PointerSessionTarget::ConnectionHandle(handle) => {
271 if let Err(rejection) = self.pointer_target_can_start_connection(handle) {
272 return PointerSessionClaimOutcome::rejected(rejection);
273 }
274 if connection_drag_threshold_met(ConnectionDragActivationInput::new(
275 input.screen_delta,
276 interaction
277 .connection_interaction()
278 .connection_drag_threshold,
279 )) {
280 PointerSessionClaimOutcome::claimed(PointerSessionClaim::Connection)
281 } else {
282 PointerSessionClaimOutcome::rejected(
283 PointerSessionClaimRejection::ActivationThresholdNotMet,
284 )
285 }
286 }
287 PointerSessionTarget::Pane { button } => {
288 let result = resolve_viewport_drag_pan_gesture(
289 &pan,
290 input.context,
291 ViewportDragPanInput::new(button, input.screen_delta),
292 );
293 match result {
294 Ok(_) => PointerSessionClaimOutcome::claimed(PointerSessionClaim::ViewportPan),
295 Err(ViewportGestureRejection::UserSelectionActive) => {
296 PointerSessionClaimOutcome::claimed(PointerSessionClaim::Selection)
297 }
298 Err(ViewportGestureRejection::ConnectionInProgress) => {
299 PointerSessionClaimOutcome::claimed(PointerSessionClaim::Connection)
300 }
301 Err(rejection) => PointerSessionClaimOutcome::rejected(
302 PointerSessionClaimRejection::ViewportGesture(rejection),
303 ),
304 }
305 }
306 }
307 }
308
309 fn pointer_target_can_start_node_drag(
310 &self,
311 node: NodeId,
312 ) -> Result<(), PointerSessionClaimRejection> {
313 let Some(node) = self.graph().nodes().get(&node) else {
314 return Err(PointerSessionClaimRejection::TargetUnavailable);
315 };
316 if node.hidden || !node.pos.is_finite() {
317 return Err(PointerSessionClaimRejection::TargetUnavailable);
318 }
319 if node
320 .parent
321 .is_some_and(|parent| self.view_state().selected_groups.contains(&parent))
322 {
323 return Err(PointerSessionClaimRejection::TargetPolicyBlocked);
324 }
325
326 if self
327 .resolved_interaction_state()
328 .node_interaction_policy(node)
329 .draggable
330 {
331 Ok(())
332 } else {
333 Err(PointerSessionClaimRejection::TargetPolicyBlocked)
334 }
335 }
336
337 fn pointer_target_can_start_connection(
338 &self,
339 handle: ConnectionHandleRef,
340 ) -> Result<(), PointerSessionClaimRejection> {
341 let Some(node) = self.graph().nodes().get(&handle.node) else {
342 return Err(PointerSessionClaimRejection::TargetUnavailable);
343 };
344 if node.hidden || !node.ports.contains(&handle.port) {
345 return Err(PointerSessionClaimRejection::TargetUnavailable);
346 }
347
348 let Some(port) = self.graph().ports().get(&handle.port) else {
349 return Err(PointerSessionClaimRejection::TargetUnavailable);
350 };
351 if port.node != handle.node || port.dir != handle.direction {
352 return Err(PointerSessionClaimRejection::TargetUnavailable);
353 }
354
355 if self
356 .resolved_interaction_state()
357 .port_interaction_policy(node, port)
358 .can_start_connection()
359 {
360 Ok(())
361 } else {
362 Err(PointerSessionClaimRejection::TargetPolicyBlocked)
363 }
364 }
365
366 pub fn apply_node_drag_session(
368 &mut self,
369 session: NodeDragSession,
370 ) -> Result<NodeDragSessionOutcome, DispatchError> {
371 let plan = self.plan_node_drag(NodeDragRequest {
372 node: session.node,
373 to: session.to,
374 });
375 let nodes = plan
376 .as_ref()
377 .map(|plan| plan.items().iter().map(|item| item.node).collect())
378 .unwrap_or_else(|| vec![session.node]);
379
380 self.emit_gesture(NodeGraphGestureEvent::NodeDragStart(NodeDragStart {
381 primary: session.node,
382 nodes: nodes.clone(),
383 pointer: session.start,
384 }));
385
386 let Some(plan) = plan else {
387 let end_outcome = self.rejected_or_noop_node_drag_outcome(session);
388 self.emit_gesture(NodeGraphGestureEvent::NodeDragEnd(NodeDragEnd {
389 primary: session.node,
390 nodes: nodes.clone(),
391 pointer: session.to,
392 outcome: end_outcome,
393 }));
394 return Ok(NodeDragSessionOutcome::without_commit(nodes, end_outcome));
395 };
396
397 match self.dispatch_transaction(plan.transaction()) {
398 Ok(committed_update) => {
399 self.emit_gesture(NodeGraphGestureEvent::NodeDragUpdate(NodeDragUpdate {
400 primary: session.node,
401 nodes: nodes.clone(),
402 pointer: session.to,
403 }));
404 self.emit_gesture(NodeGraphGestureEvent::NodeDragEnd(NodeDragEnd {
405 primary: session.node,
406 nodes: nodes.clone(),
407 pointer: session.to,
408 outcome: NodeDragEndOutcome::Committed,
409 }));
410 Ok(NodeDragSessionOutcome::committed(nodes, committed_update))
411 }
412 Err(err) => {
413 self.emit_gesture(NodeGraphGestureEvent::NodeDragEnd(NodeDragEnd {
414 primary: session.node,
415 nodes,
416 pointer: session.to,
417 outcome: NodeDragEndOutcome::Rejected,
418 }));
419 Err(err)
420 }
421 }
422 }
423
424 pub fn apply_connect_edge_session(
426 &mut self,
427 session: ConnectEdgeSession,
428 ) -> Result<ConnectSessionOutcome, ConnectEdgeError> {
429 self.emit_gesture(NodeGraphGestureEvent::ConnectStart(session.start.clone()));
430
431 match self.apply_connect_edge(session.request) {
432 Ok(Some(committed_update)) => {
433 let lifecycle = resolve_connection_lifecycle(
434 session.start,
435 self.connect_session_hover(session.request, true),
436 ConnectionEndIntent::Complete,
437 );
438 self.emit_gesture(NodeGraphGestureEvent::ConnectEnd(lifecycle.end.clone()));
439 Ok(ConnectSessionOutcome::committed(
440 lifecycle,
441 committed_update,
442 ))
443 }
444 Ok(None) => {
445 let lifecycle = self.connect_session_noop_lifecycle(session.start, session.request);
446 self.emit_gesture(NodeGraphGestureEvent::ConnectEnd(lifecycle.end.clone()));
447 Ok(ConnectSessionOutcome::without_commit(lifecycle))
448 }
449 Err(err) => {
450 let lifecycle = resolve_connection_lifecycle(
451 session.start,
452 self.connect_session_hover(session.request, false),
453 ConnectionEndIntent::Complete,
454 );
455 self.emit_gesture(NodeGraphGestureEvent::ConnectEnd(lifecycle.end));
456 Err(err)
457 }
458 }
459 }
460
461 fn connect_session_hover(
462 &self,
463 request: ConnectEdgeRequest,
464 is_handle_valid: bool,
465 ) -> Option<ResolvedConnectionTarget> {
466 let from_port = self.graph().ports().get(&request.from)?;
467 let to_port = self.graph().ports().get(&request.to)?;
468 let from = ConnectionHandleRef::new(from_port.node, request.from, from_port.dir);
469 let target_handle = ConnectionHandleRef::new(to_port.node, request.to, to_port.dir);
470 let target = ConnectionTargetHandle::new(target_handle, true, is_handle_valid);
471
472 Some(ResolvedConnectionTarget {
473 target: Some(target),
474 connection: Some(ConnectionHandleConnection::from_start_and_target(
475 from,
476 target_handle,
477 )),
478 is_handle_valid,
479 feedback: connection_handle_validity(true, is_handle_valid),
480 })
481 }
482
483 fn connect_session_noop_lifecycle(
484 &self,
485 start: ConnectStart,
486 request: ConnectEdgeRequest,
487 ) -> ConnectionLifecycleResult {
488 let hover = self.connect_session_hover(request, true);
489 let connection = hover.and_then(|hover| hover.connection);
490 let target = hover
491 .and_then(|hover| hover.target)
492 .map(|target| target.handle.port)
493 .or(Some(request.to));
494 ConnectionLifecycleResult {
495 end: ConnectEnd {
496 kind: start.kind.clone(),
497 mode: start.mode,
498 target,
499 outcome: ConnectEndOutcome::NoOp,
500 },
501 start,
502 state: ConnectionLifecycleState::NoOp,
503 hover,
504 connection,
505 target,
506 dropped_at: None,
507 dropped_wire_menu: None,
508 }
509 }
510
511 pub fn apply_viewport_drag_pan_session(
513 &mut self,
514 session: ViewportDragPanSession,
515 ) -> Result<ViewportGestureSessionOutcome, ViewportGestureRejection> {
516 let interaction = self.resolved_interaction_state();
517 let intent = resolve_viewport_drag_pan_gesture(
518 &interaction.pan_interaction(),
519 session.context,
520 session.input,
521 )?;
522 self.apply_viewport_gesture_session(intent)
523 }
524
525 fn apply_viewport_gesture_session(
526 &mut self,
527 intent: ViewportGestureIntent,
528 ) -> Result<ViewportGestureSessionOutcome, ViewportGestureRejection> {
529 let kind = intent.move_kind();
530 let start = ViewportTransform::from_view_state(self.view_state())
531 .ok_or(ViewportGestureRejection::InvalidInput)?;
532 self.emit_gesture(NodeGraphGestureEvent::ViewportMoveStart(
533 ViewportMoveStart {
534 kind,
535 pan: start.pan,
536 zoom: start.zoom,
537 },
538 ));
539
540 if !intent.apply_to_store(self) {
541 self.emit_gesture(NodeGraphGestureEvent::ViewportMoveEnd(ViewportMoveEnd {
542 kind,
543 pan: start.pan,
544 zoom: start.zoom,
545 outcome: ViewportMoveEndOutcome::Canceled,
546 }));
547 return Err(ViewportGestureRejection::InvalidInput);
548 }
549
550 let transform = ViewportTransform::from_view_state(self.view_state())
551 .ok_or(ViewportGestureRejection::InvalidInput)?;
552 self.emit_gesture(NodeGraphGestureEvent::ViewportMove(ViewportMove {
553 kind,
554 pan: transform.pan,
555 zoom: transform.zoom,
556 }));
557 self.emit_gesture(NodeGraphGestureEvent::ViewportMoveEnd(ViewportMoveEnd {
558 kind,
559 pan: transform.pan,
560 zoom: transform.zoom,
561 outcome: ViewportMoveEndOutcome::Ended,
562 }));
563
564 Ok(ViewportGestureSessionOutcome { kind, transform })
565 }
566
567 fn rejected_or_noop_node_drag_outcome(&self, session: NodeDragSession) -> NodeDragEndOutcome {
568 let Some(node) = self.graph().nodes().get(&session.node) else {
569 return NodeDragEndOutcome::Rejected;
570 };
571 if node.hidden || !session.to.is_finite() {
572 return NodeDragEndOutcome::Rejected;
573 }
574 if node.pos == session.to {
575 NodeDragEndOutcome::NoOp
576 } else {
577 NodeDragEndOutcome::Rejected
578 }
579 }
580}
581
582fn pointer_claim_reaches_node_drag(input: PointerGestureClaimInput) -> bool {
583 matches!(
584 resolve_pointer_gesture_claim(input),
585 DragPointerGestureClaim::NodeDrag
586 )
587}