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