1use super::{ControllerContext, InputController};
2use crate::event::{ExternalDragEvent, InputEvent, PointerEvent};
3use crate::scrollbar::{
4 scrollbar_drag_offset, scrollbar_drag_offset_with_grab, scrollbar_geometry_for_node,
5 scrollbar_hit_test, scrollbar_point_for_node, ScrollbarDragState, ScrollbarHitKind,
6};
7use crate::{Action, ActionEnvelope, ActionId, ActionInput, DragSessionPayload, DragSessionState};
8use fission_ir::op::RichTextAnnotation;
9use fission_ir::{semantics::ActionTrigger, Op, WidgetId};
10use fission_layout::{LayoutPoint, LayoutSnapshot};
11
12pub(crate) fn cancel_active_drag_for_viewport(
13 ir: &fission_ir::CoreIR,
14 layout: &LayoutSnapshot,
15 viewport: &crate::input::viewport::ViewportStateMap,
16 gesture: &crate::env::GestureState,
17 point: LayoutPoint,
18 dispatched_actions: &mut Vec<(WidgetId, ActionEnvelope, ActionInput)>,
19) {
20 let Some(start_node) = gesture.target_node.filter(|_| gesture.is_panning) else {
21 return;
22 };
23 let mut current_id = Some(start_node);
24 while let Some(node_id) = current_id {
25 let Some(node) = ir.nodes.get(&node_id) else {
26 break;
27 };
28 if let Op::Semantics(semantics) = &node.op {
29 if let Some(entry) = semantics
30 .actions
31 .entries
32 .iter()
33 .find(|entry| entry.trigger == ActionTrigger::DragEnd)
34 {
35 let input = if let Some(target) = &semantics.canvas_target {
36 ActionInput::CanvasInteraction(crate::input::canvas::canvas_interaction(
37 node_id,
38 target,
39 crate::input::canvas::CanvasInteractionPhase::Cancel,
40 point,
41 LayoutPoint::ZERO,
42 gesture.start_point,
43 layout,
44 viewport,
45 gesture.pointer_kind,
46 gesture.modifiers,
47 ))
48 } else {
49 ActionInput::Pointer {
50 x: point.x,
51 y: point.y,
52 delta_x: 0.0,
53 delta_y: 0.0,
54 }
55 };
56 dispatched_actions.push((
57 node_id,
58 ActionEnvelope {
59 id: ActionId::from_u128(entry.action_id),
60 payload: entry.payload_data.clone().unwrap_or_default(),
61 },
62 crate::input::scoped_action_input(ir, node_id, input),
63 ));
64 return;
65 }
66 }
67 current_id = node.parent;
68 }
69}
70
71pub struct GestureController;
72
73impl InputController for GestureController {
74 fn handle_event(&mut self, ctx: &mut ControllerContext, event: &InputEvent) -> bool {
75 match event {
76 InputEvent::Pointer(pe) => {
77 match pe {
78 PointerEvent::Down {
79 point,
80 button,
81 kind,
82 modifiers,
83 ..
84 } => {
85 if ctx.gesture.pressed_button.is_some() {
89 return true;
90 }
91
92 ctx.gesture.start_point = Some(*point);
93 ctx.gesture.last_point = Some(*point);
94 ctx.gesture.is_panning = false;
95 ctx.gesture.pressed_button = Some(button.clone());
96 ctx.gesture.pointer_kind = *kind;
97 ctx.gesture.modifiers = *modifiers;
98 ctx.gesture.scrollbar_drag = None;
99
100 if matches!(button, crate::event::PointerButton::Primary) {
101 if let Some(hit) =
102 scrollbar_hit_test(ctx.ir, ctx.layout, ctx.scroll, *point)
103 {
104 let pointer_to_thumb_start = match hit.kind {
105 ScrollbarHitKind::Thumb => hit.pointer_to_thumb_start,
106 ScrollbarHitKind::Rail => hit.geometry.thumb_extent() * 0.5,
107 };
108 let new_offset = match hit.kind {
109 ScrollbarHitKind::Thumb => hit.geometry.offset,
110 ScrollbarHitKind::Rail => {
111 scrollbar_drag_offset(hit.geometry, hit.layout_point)
112 }
113 };
114 ctx.scroll.set_offset(hit.geometry.node_id, new_offset);
115 ctx.gesture.target_node = Some(hit.geometry.node_id);
116 ctx.gesture.dragging_payload = None;
117 ctx.gesture.scrollbar_drag = Some(ScrollbarDragState {
118 node_id: hit.geometry.node_id,
119 pointer_to_thumb_start,
120 });
121 return true;
122 }
123 }
124
125 if let Some(hit) = crate::hit_test::hit_test_with_viewports(
126 ctx.ir,
127 ctx.layout,
128 ctx.scroll,
129 ctx.viewport,
130 *point,
131 ) {
132 ctx.gesture.target_node = Some(hit);
133 ctx.gesture.dragging_payload =
134 matches!(button, crate::event::PointerButton::Primary)
135 .then(|| self.find_drag_payload(ctx, hit))
136 .flatten();
137 } else {
138 ctx.gesture.target_node = None;
139 ctx.gesture.dragging_payload = None;
140 }
141 }
142 PointerEvent::Move {
143 point,
144 kind,
145 modifiers,
146 ..
147 } => {
148 ctx.gesture.pointer_kind = *kind;
149 ctx.gesture.modifiers = *modifiers;
150 if !matches!(
151 ctx.gesture.pressed_button,
152 Some(crate::event::PointerButton::Primary)
153 ) {
154 return false;
155 }
156
157 if let Some(drag) = ctx.gesture.scrollbar_drag {
158 if let Some(geometry) = scrollbar_geometry_for_node(
159 ctx.ir,
160 ctx.layout,
161 ctx.scroll,
162 drag.node_id,
163 ) {
164 let new_offset = scrollbar_drag_offset_with_grab(
165 geometry,
166 scrollbar_point_for_node(
167 ctx.ir,
168 ctx.scroll,
169 drag.node_id,
170 *point,
171 ),
172 drag.pointer_to_thumb_start,
173 );
174 ctx.scroll.set_offset(drag.node_id, new_offset);
175 }
176 ctx.gesture.last_point = Some(*point);
177 return true;
178 }
179
180 if let Some(start) = ctx.gesture.start_point {
181 let dx = point.x - start.x;
182 let dy = point.y - start.y;
183 let dist_sq = dx * dx + dy * dy;
184 let threshold = 5.0 * 5.0;
185
186 if !ctx.gesture.is_panning && dist_sq > threshold {
187 ctx.gesture.is_panning = true;
188 if let Some(payload) = ctx.gesture.dragging_payload.clone() {
189 let target = ctx.gesture.target_node;
190 let source_identifier =
191 target.and_then(|id| self.semantic_identifier(ctx, id));
192 ctx.gesture.drag_session = Some(DragSessionState {
193 source_node: target,
194 source_identifier,
195 payload: DragSessionPayload::Internal(payload),
196 point: *point,
197 target_node: None,
198 target_identifier: None,
199 });
200 self.update_drag_target(ctx, *point);
201 }
202 if let Some(target) = ctx.gesture.target_node {
204 self.dispatch_trigger(
205 ctx,
206 target,
207 ActionTrigger::DragStart,
208 *point,
209 None,
210 );
211 }
212 }
213
214 if ctx.gesture.is_panning {
215 if let Some(session) = ctx.gesture.drag_session.as_mut() {
216 session.point = *point;
217 }
218 self.update_drag_target(ctx, *point);
219
220 let last = ctx.gesture.last_point.unwrap_or(start);
221 let delta = LayoutPoint {
222 x: point.x - last.x,
223 y: point.y - last.y,
224 };
225 ctx.gesture.last_point = Some(*point);
226
227 let dispatched = if let Some(target) = ctx.gesture.target_node {
229 self.dispatch_trigger(
230 ctx,
231 target,
232 ActionTrigger::DragUpdate,
233 *point,
234 Some(delta),
235 )
236 } else {
237 false
238 };
239
240 if dispatched {
241 return true;
242 }
243
244 if self.handle_pan_update(ctx, delta) {
246 return true;
247 }
248 }
249 }
250 }
251 PointerEvent::Up {
252 point,
253 button,
254 kind,
255 modifiers,
256 ..
257 } => {
258 ctx.gesture.pointer_kind = *kind;
259 ctx.gesture.modifiers = *modifiers;
260 let scrollbar_drag = ctx.gesture.scrollbar_drag.take();
261 let mut handled = false;
262 let pressed_button = ctx.gesture.pressed_button.clone();
263 let buttons_match = pressed_button.as_ref() == Some(button);
264 let was_primary =
265 matches!(pressed_button, Some(crate::event::PointerButton::Primary));
266 let was_secondary =
267 matches!(pressed_button, Some(crate::event::PointerButton::Secondary));
268
269 if pressed_button.is_some() && !buttons_match {
270 self.reset_pointer_sequence(ctx, *point);
271 return true;
272 }
273
274 if buttons_match && ctx.gesture.is_panning {
275 if let Some(payload) = ctx.gesture.dragging_payload.take() {
277 if let Some(up_hit) = crate::hit_test::hit_test_with_viewports(
278 ctx.ir,
279 ctx.layout,
280 ctx.scroll,
281 ctx.viewport,
282 *point,
283 ) {
284 let _ = self.dispatch_internal_drop(
285 ctx, up_hit, payload, *point, *modifiers,
286 );
287 }
288 }
289
290 if let Some(target) = ctx.gesture.target_node {
291 self.dispatch_trigger(
292 ctx,
293 target,
294 ActionTrigger::DragEnd,
295 *point,
296 None,
297 );
298 }
299 handled = true;
300 } else if buttons_match && was_secondary {
301 if let Some(target) = ctx.gesture.target_node {
303 if let Some(up_hit) = crate::hit_test::hit_test_with_viewports(
304 ctx.ir,
305 ctx.layout,
306 ctx.scroll,
307 ctx.viewport,
308 *point,
309 ) {
310 if up_hit == target
311 || self.is_descendant(ctx, up_hit, target)
312 || self.is_descendant(ctx, target, up_hit)
313 {
314 if let Some(menu_owner) =
315 self.find_context_menu_owner(ctx, up_hit)
316 {
317 ctx.context_menu.open(menu_owner, *point);
318 handled = true;
319 }
320
321 let rich_text_path = self.path_for_node(ctx, up_hit);
322 if !handled {
323 if let Some((annotation_node_id, annotation)) =
324 crate::input::hover::resolve_rich_text_annotation_at_point(
325 ctx,
326 &rich_text_path,
327 *point,
328 )
329 {
330 handled = self.dispatch_annotation_trigger(
331 ctx,
332 annotation_node_id,
333 &annotation,
334 ActionTrigger::SecondaryClick,
335 *point,
336 );
337 }
338 }
339
340 if !handled
341 && self.dispatch_trigger(
342 ctx,
343 target,
344 ActionTrigger::SecondaryClick,
345 *point,
346 None,
347 )
348 {
349 handled = true;
350 }
351 }
352 }
353 }
354 } else if buttons_match && was_primary {
355 if let Some(target) = ctx.gesture.target_node {
357 if let Some(up_hit) = crate::hit_test::hit_test_with_viewports(
358 ctx.ir,
359 ctx.layout,
360 ctx.scroll,
361 ctx.viewport,
362 *point,
363 ) {
364 if up_hit == target
365 || self.is_descendant(ctx, up_hit, target)
366 || self.is_descendant(ctx, target, up_hit)
367 {
368 let rich_text_path = self.path_for_node(ctx, up_hit);
369 if let Some((annotation_node_id, annotation)) =
370 crate::input::hover::resolve_rich_text_annotation_at_point(
371 ctx,
372 &rich_text_path,
373 *point,
374 )
375 {
376 handled = self.dispatch_annotation_trigger(
377 ctx,
378 annotation_node_id,
379 &annotation,
380 ActionTrigger::Default,
381 *point,
382 );
383 }
384
385 if !handled
386 && self.dispatch_trigger(
387 ctx,
388 target,
389 ActionTrigger::Default,
390 *point,
391 None,
392 )
393 {
394 handled = true;
395 }
396 }
397 }
398 }
399 }
400
401 if !was_secondary {
402 ctx.context_menu.close();
403 }
404 self.reset_pointer_sequence(ctx, *point);
405 if scrollbar_drag.is_some() {
406 ctx.gesture.target_node = None;
407 return true;
408 }
409 return handled;
410 }
411 PointerEvent::Cancel {
412 point,
413 kind,
414 modifiers,
415 ..
416 } => {
417 let had_sequence = ctx.gesture.pressed_button.is_some()
418 || ctx.gesture.drag_session.is_some()
419 || ctx.gesture.scrollbar_drag.is_some();
420 ctx.gesture.pointer_kind = *kind;
421 ctx.gesture.modifiers = *modifiers;
422 if ctx.gesture.is_panning {
423 if let Some(target) = ctx.gesture.target_node {
424 self.dispatch_trigger_with_phase(
425 ctx,
426 target,
427 ActionTrigger::DragEnd,
428 *point,
429 None,
430 Some(crate::input::canvas::CanvasInteractionPhase::Cancel),
431 );
432 }
433 }
434 ctx.gesture.scrollbar_drag = None;
435 self.reset_pointer_sequence(ctx, *point);
436 ctx.gesture.target_node = None;
437 return had_sequence;
438 }
439 _ => {}
440 }
441 }
442 InputEvent::ExternalDrag(event) => match event {
443 ExternalDragEvent::Hover { point, paths, .. } => {
444 ctx.gesture.drag_session = Some(DragSessionState {
445 source_node: None,
446 source_identifier: None,
447 payload: DragSessionPayload::ExternalFiles(paths.clone()),
448 point: *point,
449 target_node: ctx
450 .gesture
451 .drag_session
452 .as_ref()
453 .and_then(|s| s.target_node),
454 target_identifier: ctx
455 .gesture
456 .drag_session
457 .as_ref()
458 .and_then(|s| s.target_identifier.clone()),
459 });
460 self.update_drag_target(ctx, *point);
461 return true;
462 }
463 ExternalDragEvent::Cancel => {
464 let point = ctx
465 .gesture
466 .drag_session
467 .as_ref()
468 .map(|session| session.point)
469 .unwrap_or(LayoutPoint::ZERO);
470 self.clear_drag_target(ctx, point);
471 ctx.gesture.drag_session = None;
472 return true;
473 }
474 ExternalDragEvent::Drop {
475 point,
476 paths,
477 modifiers,
478 } => {
479 ctx.gesture.drag_session = Some(DragSessionState {
480 source_node: None,
481 source_identifier: None,
482 payload: DragSessionPayload::ExternalFiles(paths.clone()),
483 point: *point,
484 target_node: ctx
485 .gesture
486 .drag_session
487 .as_ref()
488 .and_then(|s| s.target_node),
489 target_identifier: ctx
490 .gesture
491 .drag_session
492 .as_ref()
493 .and_then(|s| s.target_identifier.clone()),
494 });
495 self.update_drag_target(ctx, *point);
496 if let Some(target) = ctx
497 .gesture
498 .drag_session
499 .as_ref()
500 .and_then(|s| s.target_node)
501 {
502 let _ = self.dispatch_external_drop(
503 ctx,
504 target,
505 paths.clone(),
506 *point,
507 *modifiers,
508 );
509 }
510 self.clear_drag_target(ctx, *point);
511 ctx.gesture.drag_session = None;
512 return true;
513 }
514 },
515 InputEvent::ContextMenuRequested { point, .. } => {
516 return self.handle_context_menu_request(ctx, *point);
517 }
518 _ => {}
519 }
520 false
521 }
522}
523
524impl GestureController {
525 fn handle_context_menu_request(
526 &mut self,
527 ctx: &mut ControllerContext,
528 point: LayoutPoint,
529 ) -> bool {
530 let Some(hit) = crate::hit_test::hit_test_with_viewports(
531 ctx.ir,
532 ctx.layout,
533 ctx.scroll,
534 ctx.viewport,
535 point,
536 ) else {
537 return false;
538 };
539 if let Some(menu_owner) = self.find_context_menu_owner(ctx, hit) {
540 ctx.context_menu.open(menu_owner, point);
541 return true;
542 }
543 let rich_text_path = self.path_for_node(ctx, hit);
544 if let Some((annotation_node_id, annotation)) =
545 crate::input::hover::resolve_rich_text_annotation_at_point(ctx, &rich_text_path, point)
546 {
547 if self.dispatch_annotation_trigger(
548 ctx,
549 annotation_node_id,
550 &annotation,
551 ActionTrigger::SecondaryClick,
552 point,
553 ) {
554 return true;
555 }
556 }
557 self.dispatch_trigger(ctx, hit, ActionTrigger::SecondaryClick, point, None)
558 }
559
560 fn reset_pointer_sequence(&self, ctx: &mut ControllerContext, point: LayoutPoint) {
561 ctx.gesture.start_point = None;
562 ctx.gesture.is_panning = false;
563 ctx.gesture.dragging_payload = None;
564 self.clear_drag_target(ctx, point);
565 ctx.gesture.drag_session = None;
566 ctx.gesture.pressed_button = None;
567 }
568
569 fn path_for_node(&self, ctx: &ControllerContext, node_id: WidgetId) -> Vec<WidgetId> {
570 let mut path = Vec::new();
571 let mut curr = Some(node_id);
572 while let Some(id) = curr {
573 path.push(id);
574 curr = ctx.ir.nodes.get(&id).and_then(|node| node.parent);
575 }
576 path
577 }
578
579 fn is_descendant(&self, ctx: &ControllerContext, child: WidgetId, ancestor: WidgetId) -> bool {
580 let mut curr = Some(child);
581 while let Some(id) = curr {
582 if id == ancestor {
583 return true;
584 }
585 if let Some(node) = ctx.ir.nodes.get(&id) {
586 curr = node.parent;
587 } else {
588 break;
589 }
590 }
591 false
592 }
593
594 fn find_context_menu_owner(
595 &self,
596 ctx: &ControllerContext,
597 start_node: WidgetId,
598 ) -> Option<WidgetId> {
599 let mut current_id = Some(start_node);
600 while let Some(node_id) = current_id {
601 let Some(node) = ctx.ir.nodes.get(&node_id) else {
602 break;
603 };
604 if let Op::Semantics(semantics) = &node.op {
605 if semantics.context_menu && !semantics.disabled {
606 return Some(node_id);
607 }
608 }
609 current_id = node.parent;
610 }
611 None
612 }
613
614 fn dispatch_annotation_trigger(
615 &self,
616 ctx: &mut ControllerContext,
617 node_id: WidgetId,
618 annotation: &RichTextAnnotation,
619 trigger: ActionTrigger,
620 point: LayoutPoint,
621 ) -> bool {
622 let Some(action_entry) = annotation
623 .actions
624 .iter()
625 .find(|entry| entry.trigger == trigger)
626 else {
627 return false;
628 };
629 let Some(payload) = &action_entry.payload_data else {
630 return false;
631 };
632
633 let input = crate::input::scoped_action_input(
634 ctx.ir,
635 node_id,
636 ActionInput::Pointer {
637 x: point.x,
638 y: point.y,
639 delta_x: 0.0,
640 delta_y: 0.0,
641 },
642 );
643 ctx.dispatched_actions.push((
644 node_id,
645 ActionEnvelope {
646 id: ActionId::from_u128(action_entry.action_id),
647 payload: payload.clone(),
648 },
649 input,
650 ));
651 true
652 }
653
654 fn find_drag_payload(&self, ctx: &ControllerContext, start_node: WidgetId) -> Option<Vec<u8>> {
655 let mut current_id = Some(start_node);
656 while let Some(node_id) = current_id {
657 if let Some(node) = ctx.ir.nodes.get(&node_id) {
658 if let Op::Semantics(sem) = &node.op {
659 if let Some(p) = &sem.drag_payload {
660 return Some(p.clone());
661 }
662 }
663 current_id = node.parent;
664 } else {
665 break;
666 }
667 }
668 None
669 }
670
671 fn semantic_identifier(&self, ctx: &ControllerContext, start_node: WidgetId) -> Option<String> {
672 let mut current_id = Some(start_node);
673 while let Some(node_id) = current_id {
674 if let Some(node) = ctx.ir.nodes.get(&node_id) {
675 if let Op::Semantics(sem) = &node.op {
676 if let Some(identifier) = &sem.identifier {
677 return Some(identifier.clone());
678 }
679 }
680 current_id = node.parent;
681 } else {
682 break;
683 }
684 }
685 None
686 }
687
688 fn find_drop_target(&self, ctx: &ControllerContext, start_node: WidgetId) -> Option<WidgetId> {
689 let mut current_id = Some(start_node);
690 while let Some(node_id) = current_id {
691 if let Some(node) = ctx.ir.nodes.get(&node_id) {
692 if let Op::Semantics(sem) = &node.op {
693 if sem
694 .actions
695 .entries
696 .iter()
697 .any(|entry| entry.trigger == ActionTrigger::Drop)
698 {
699 return Some(node_id);
700 }
701 }
702 current_id = node.parent;
703 } else {
704 break;
705 }
706 }
707 None
708 }
709
710 fn update_drag_target(&self, ctx: &mut ControllerContext, point: LayoutPoint) {
711 let next_target = crate::hit_test::hit_test_with_viewports(
712 ctx.ir,
713 ctx.layout,
714 ctx.scroll,
715 ctx.viewport,
716 point,
717 )
718 .and_then(|hit| self.find_drop_target(ctx, hit));
719
720 let previous_target = ctx
721 .gesture
722 .drag_session
723 .as_ref()
724 .and_then(|s| s.target_node);
725 if previous_target == next_target {
726 return;
727 }
728
729 if let Some(previous) = previous_target {
730 self.dispatch_trigger(ctx, previous, ActionTrigger::DragLeave, point, None);
731 }
732 if let Some(next) = next_target {
733 self.dispatch_trigger(ctx, next, ActionTrigger::DragEnter, point, None);
734 }
735
736 let next_identifier = next_target.and_then(|id| self.semantic_identifier(ctx, id));
737 if let Some(session) = ctx.gesture.drag_session.as_mut() {
738 session.target_node = next_target;
739 session.target_identifier = next_identifier;
740 }
741 }
742
743 fn clear_drag_target(&self, ctx: &mut ControllerContext, point: LayoutPoint) {
744 if let Some(previous) = ctx
745 .gesture
746 .drag_session
747 .as_ref()
748 .and_then(|s| s.target_node)
749 {
750 self.dispatch_trigger(ctx, previous, ActionTrigger::DragLeave, point, None);
751 }
752 if let Some(session) = ctx.gesture.drag_session.as_mut() {
753 session.target_node = None;
754 session.target_identifier = None;
755 }
756 }
757
758 fn dispatch_internal_drop(
759 &self,
760 ctx: &mut ControllerContext,
761 target_node: WidgetId,
762 payload: Vec<u8>,
763 point: LayoutPoint,
764 modifiers: u8,
765 ) -> bool {
766 let mut current_id = Some(target_node);
767 while let Some(node_id) = current_id {
768 if let Some(node) = ctx.ir.nodes.get(&node_id) {
769 if let Op::Semantics(sem) = &node.op {
770 for entry in &sem.actions.entries {
771 if entry.trigger == ActionTrigger::Drop {
772 let envelope = ActionEnvelope {
773 id: ActionId::from_u128(entry.action_id),
774 payload: entry.payload_data.clone().unwrap_or_default(),
775 };
776
777 let input = crate::input::scoped_action_input(
778 ctx.ir,
779 node_id,
780 ActionInput::InternalDrop {
781 payload: payload.clone(),
782 x: point.x,
783 y: point.y,
784 modifiers,
785 },
786 );
787
788 ctx.dispatched_actions.push((node_id, envelope, input));
789 return true;
790 }
791 }
792 }
793 current_id = node.parent;
794 } else {
795 break;
796 }
797 }
798 false
799 }
800
801 fn dispatch_external_drop(
802 &self,
803 ctx: &mut ControllerContext,
804 target_node: WidgetId,
805 paths: Vec<String>,
806 point: LayoutPoint,
807 modifiers: u8,
808 ) -> bool {
809 let mut current_id = Some(target_node);
810 while let Some(node_id) = current_id {
811 if let Some(node) = ctx.ir.nodes.get(&node_id) {
812 if let Op::Semantics(sem) = &node.op {
813 for entry in &sem.actions.entries {
814 if entry.trigger == ActionTrigger::Drop {
815 let envelope = ActionEnvelope {
816 id: ActionId::from_u128(entry.action_id),
817 payload: entry.payload_data.clone().unwrap_or_default(),
818 };
819
820 let input = crate::input::scoped_action_input(
821 ctx.ir,
822 node_id,
823 ActionInput::Drop {
824 paths: paths.clone(),
825 x: point.x,
826 y: point.y,
827 modifiers,
828 },
829 );
830
831 ctx.dispatched_actions.push((node_id, envelope, input));
832 return true;
833 }
834 }
835 }
836 current_id = node.parent;
837 } else {
838 break;
839 }
840 }
841 false
842 }
843
844 fn dispatch_trigger(
845 &self,
846 ctx: &mut ControllerContext,
847 start_node: WidgetId,
848 trigger: ActionTrigger,
849 point: LayoutPoint,
850 delta: Option<LayoutPoint>,
851 ) -> bool {
852 self.dispatch_trigger_with_phase(ctx, start_node, trigger, point, delta, None)
853 }
854
855 fn dispatch_trigger_with_phase(
856 &self,
857 ctx: &mut ControllerContext,
858 start_node: WidgetId,
859 trigger: ActionTrigger,
860 point: LayoutPoint,
861 delta: Option<LayoutPoint>,
862 phase: Option<crate::input::canvas::CanvasInteractionPhase>,
863 ) -> bool {
864 let mut current_id = Some(start_node);
865 while let Some(node_id) = current_id {
866 if let Some(node) = ctx.ir.nodes.get(&node_id) {
867 if let Op::Semantics(sem) = &node.op {
868 let mut handled = false;
869 for entry in &sem.actions.entries {
870 if entry.trigger == trigger {
871 let envelope = ActionEnvelope {
872 id: ActionId::from_u128(entry.action_id),
873 payload: entry.payload_data.clone().unwrap_or_default(),
874 };
875
876 let delta = delta.unwrap_or(LayoutPoint::ZERO);
877 let input = if let Some(target) = &sem.canvas_target {
878 ActionInput::CanvasInteraction(
879 crate::input::canvas::canvas_interaction(
880 node_id,
881 target,
882 phase.unwrap_or_else(|| canvas_phase(trigger)),
883 point,
884 delta,
885 ctx.gesture.start_point,
886 ctx.layout,
887 ctx.viewport,
888 ctx.gesture.pointer_kind,
889 ctx.gesture.modifiers,
890 ),
891 )
892 } else {
893 ActionInput::Pointer {
894 x: point.x,
895 y: point.y,
896 delta_x: delta.x,
897 delta_y: delta.y,
898 }
899 };
900 let input = crate::input::scoped_action_input(ctx.ir, node_id, input);
901
902 ctx.dispatched_actions.push((node_id, envelope, input));
903 handled = true;
904 break;
905 }
906 }
907 if trigger == ActionTrigger::Default {
908 if let Some(hyperlink) = &sem.hyperlink {
909 let navigation_already_bound =
910 sem.actions.entries.iter().any(|entry| {
911 entry.trigger == ActionTrigger::Default
912 && ActionId::from_u128(entry.action_id)
913 == crate::NavigationRequested::static_id()
914 });
915 if !navigation_already_bound {
916 ctx.dispatched_actions.push((
917 node_id,
918 crate::NavigationRequested::new(
919 crate::NavigationCommand::Open(hyperlink.clone()),
920 )
921 .into(),
922 crate::input::scoped_action_input(
923 ctx.ir,
924 node_id,
925 ActionInput::Pointer {
926 x: point.x,
927 y: point.y,
928 delta_x: 0.0,
929 delta_y: 0.0,
930 },
931 ),
932 ));
933 }
934 handled = true;
935 }
936 }
937 if handled {
938 return true;
939 }
940 }
941 current_id = node.parent;
942 } else {
943 break;
944 }
945 }
946 false
947 }
948
949 fn handle_pan_update(&self, ctx: &mut ControllerContext, delta: LayoutPoint) -> bool {
950 if let Some(target) = ctx.gesture.target_node {
951 let mut current = Some(target);
952 while let Some(id) = current {
953 if let Some(node) = ctx.ir.nodes.get(&id) {
954 if let fission_ir::Op::Semantics(sem) = &node.op {
955 if sem.draggable {
956 return false;
957 }
958 }
959 if let fission_ir::Op::Layout(fission_ir::op::LayoutOp::Scroll {
960 direction,
961 ..
962 }) = &node.op
963 {
964 let current_offset = ctx.scroll.get_offset(id);
965 let move_val = match direction {
966 fission_ir::op::FlexDirection::Row => -delta.x,
967 fission_ir::op::FlexDirection::Column => -delta.y,
968 };
969
970 let mut new_offset = current_offset + move_val;
971
972 if let Some(geom) = ctx.layout.get_node_geometry(id) {
973 let max_offset =
974 if matches!(direction, fission_ir::op::FlexDirection::Row) {
975 (geom.content_size.width - geom.rect.width()).max(0.0)
976 } else {
977 (geom.content_size.height - geom.rect.height()).max(0.0)
978 };
979 new_offset = new_offset.clamp(0.0, max_offset);
980 }
981
982 ctx.scroll.set_offset(id, new_offset);
983 return true;
984 }
985 current = node.parent;
986 } else {
987 break;
988 }
989 }
990 }
991 false
992 }
993}
994
995fn canvas_phase(trigger: ActionTrigger) -> crate::input::canvas::CanvasInteractionPhase {
996 use crate::input::canvas::CanvasInteractionPhase;
997 match trigger {
998 ActionTrigger::DragStart => CanvasInteractionPhase::Start,
999 ActionTrigger::DragUpdate => CanvasInteractionPhase::Update,
1000 ActionTrigger::DragEnd => CanvasInteractionPhase::End,
1001 _ => CanvasInteractionPhase::Activate,
1002 }
1003}