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::{ActionEnvelope, ActionId, ActionInput, DragSessionPayload, DragSessionState};
8use fission_ir::op::RichTextAnnotation;
9use fission_ir::{semantics::ActionTrigger, Op, WidgetId};
10use fission_layout::LayoutPoint;
11
12pub struct GestureController;
13
14impl InputController for GestureController {
15 fn handle_event(&mut self, ctx: &mut ControllerContext, event: &InputEvent) -> bool {
16 match event {
17 InputEvent::Pointer(pe) => {
18 match pe {
19 PointerEvent::Down { point, button, .. } => {
20 ctx.gesture.start_point = Some(*point);
21 ctx.gesture.last_point = Some(*point);
22 ctx.gesture.is_panning = false;
23 ctx.gesture.pressed_button = Some(button.clone());
24 ctx.gesture.scrollbar_drag = None;
25
26 if matches!(button, crate::event::PointerButton::Primary) {
27 if let Some(hit) =
28 scrollbar_hit_test(ctx.ir, ctx.layout, ctx.scroll, *point)
29 {
30 let pointer_to_thumb_start = match hit.kind {
31 ScrollbarHitKind::Thumb => hit.pointer_to_thumb_start,
32 ScrollbarHitKind::Rail => hit.geometry.thumb_extent() * 0.5,
33 };
34 let new_offset = match hit.kind {
35 ScrollbarHitKind::Thumb => hit.geometry.offset,
36 ScrollbarHitKind::Rail => {
37 scrollbar_drag_offset(hit.geometry, hit.layout_point)
38 }
39 };
40 ctx.scroll.set_offset(hit.geometry.node_id, new_offset);
41 ctx.gesture.target_node = Some(hit.geometry.node_id);
42 ctx.gesture.dragging_payload = None;
43 ctx.gesture.scrollbar_drag = Some(ScrollbarDragState {
44 node_id: hit.geometry.node_id,
45 pointer_to_thumb_start,
46 });
47 return true;
48 }
49 }
50
51 if let Some(hit) = crate::hit_test::hit_test_with_scroll(
52 ctx.ir, ctx.layout, ctx.scroll, *point,
53 ) {
54 ctx.gesture.target_node = Some(hit);
55 ctx.gesture.dragging_payload = self.find_drag_payload(ctx, hit);
56 } else {
57 ctx.gesture.target_node = None;
58 ctx.gesture.dragging_payload = None;
59 }
60 }
61 PointerEvent::Move { point, .. } => {
62 if let Some(drag) = ctx.gesture.scrollbar_drag {
63 if let Some(geometry) = scrollbar_geometry_for_node(
64 ctx.ir,
65 ctx.layout,
66 ctx.scroll,
67 drag.node_id,
68 ) {
69 let new_offset = scrollbar_drag_offset_with_grab(
70 geometry,
71 scrollbar_point_for_node(
72 ctx.ir,
73 ctx.scroll,
74 drag.node_id,
75 *point,
76 ),
77 drag.pointer_to_thumb_start,
78 );
79 ctx.scroll.set_offset(drag.node_id, new_offset);
80 }
81 ctx.gesture.last_point = Some(*point);
82 return true;
83 }
84
85 if let Some(start) = ctx.gesture.start_point {
86 let dx = point.x - start.x;
87 let dy = point.y - start.y;
88 let dist_sq = dx * dx + dy * dy;
89 let threshold = 5.0 * 5.0;
90
91 if !ctx.gesture.is_panning && dist_sq > threshold {
92 ctx.gesture.is_panning = true;
93 if let Some(payload) = ctx.gesture.dragging_payload.clone() {
94 let target = ctx.gesture.target_node;
95 let source_identifier =
96 target.and_then(|id| self.semantic_identifier(ctx, id));
97 ctx.gesture.drag_session = Some(DragSessionState {
98 source_node: target,
99 source_identifier,
100 payload: DragSessionPayload::Internal(payload),
101 point: *point,
102 target_node: None,
103 target_identifier: None,
104 });
105 self.update_drag_target(ctx, *point);
106 }
107 if let Some(target) = ctx.gesture.target_node {
109 self.dispatch_trigger(
110 ctx,
111 target,
112 ActionTrigger::DragStart,
113 *point,
114 None,
115 );
116 }
117 }
118
119 if ctx.gesture.is_panning {
120 if let Some(session) = ctx.gesture.drag_session.as_mut() {
121 session.point = *point;
122 }
123 self.update_drag_target(ctx, *point);
124
125 let last = ctx.gesture.last_point.unwrap_or(start);
126 let delta = LayoutPoint {
127 x: point.x - last.x,
128 y: point.y - last.y,
129 };
130 ctx.gesture.last_point = Some(*point);
131
132 let dispatched = if let Some(target) = ctx.gesture.target_node {
134 self.dispatch_trigger(
135 ctx,
136 target,
137 ActionTrigger::DragUpdate,
138 *point,
139 Some(delta),
140 )
141 } else {
142 false
143 };
144
145 if dispatched {
146 return true;
147 }
148
149 if self.handle_pan_update(ctx, delta) {
151 return true;
152 }
153 }
154 }
155 }
156 PointerEvent::Up {
157 point, modifiers, ..
158 } => {
159 let scrollbar_drag = ctx.gesture.scrollbar_drag.take();
160 let mut handled = false;
161 let was_secondary = matches!(
162 ctx.gesture.pressed_button,
163 Some(crate::event::PointerButton::Secondary)
164 );
165 if ctx.gesture.is_panning {
166 if let Some(payload) = ctx.gesture.dragging_payload.take() {
168 if let Some(up_hit) = crate::hit_test::hit_test_with_scroll(
169 ctx.ir, ctx.layout, ctx.scroll, *point,
170 ) {
171 let _ = self.dispatch_internal_drop(
172 ctx, up_hit, payload, *point, *modifiers,
173 );
174 }
175 }
176
177 if let Some(target) = ctx.gesture.target_node {
178 self.dispatch_trigger(
179 ctx,
180 target,
181 ActionTrigger::DragEnd,
182 *point,
183 None,
184 );
185 }
186 handled = true;
187 } else if was_secondary {
188 if let Some(target) = ctx.gesture.target_node {
190 if let Some(up_hit) = crate::hit_test::hit_test_with_scroll(
191 ctx.ir, ctx.layout, ctx.scroll, *point,
192 ) {
193 if up_hit == target
194 || self.is_descendant(ctx, up_hit, target)
195 || self.is_descendant(ctx, target, up_hit)
196 {
197 if let Some(menu_owner) =
198 self.find_context_menu_owner(ctx, up_hit)
199 {
200 ctx.context_menu.open(menu_owner, *point);
201 handled = true;
202 }
203
204 let rich_text_path = self.path_for_node(ctx, up_hit);
205 if !handled {
206 if let Some((annotation_node_id, annotation)) =
207 crate::input::hover::resolve_rich_text_annotation_at_point(
208 ctx,
209 &rich_text_path,
210 *point,
211 )
212 {
213 handled = self.dispatch_annotation_trigger(
214 ctx,
215 annotation_node_id,
216 &annotation,
217 ActionTrigger::SecondaryClick,
218 *point,
219 );
220 }
221 }
222
223 if !handled
224 && self.dispatch_trigger(
225 ctx,
226 target,
227 ActionTrigger::SecondaryClick,
228 *point,
229 None,
230 )
231 {
232 handled = true;
233 }
234 }
235 }
236 }
237 } else {
238 if let Some(target) = ctx.gesture.target_node {
240 if let Some(up_hit) = crate::hit_test::hit_test_with_scroll(
241 ctx.ir, ctx.layout, ctx.scroll, *point,
242 ) {
243 if up_hit == target
244 || self.is_descendant(ctx, up_hit, target)
245 || self.is_descendant(ctx, target, up_hit)
246 {
247 let rich_text_path = self.path_for_node(ctx, up_hit);
248 if let Some((annotation_node_id, annotation)) =
249 crate::input::hover::resolve_rich_text_annotation_at_point(
250 ctx,
251 &rich_text_path,
252 *point,
253 )
254 {
255 handled = self.dispatch_annotation_trigger(
256 ctx,
257 annotation_node_id,
258 &annotation,
259 ActionTrigger::Default,
260 *point,
261 );
262 }
263
264 if !handled
265 && self.dispatch_trigger(
266 ctx,
267 target,
268 ActionTrigger::Default,
269 *point,
270 None,
271 )
272 {
273 handled = true;
274 }
275 }
276 }
277 }
278 }
279
280 if !was_secondary {
281 ctx.context_menu.close();
282 }
283 ctx.gesture.start_point = None;
284 ctx.gesture.is_panning = false;
285 ctx.gesture.dragging_payload = None;
286 self.clear_drag_target(ctx, *point);
287 ctx.gesture.drag_session = None;
288 ctx.gesture.pressed_button = None;
289 if scrollbar_drag.is_some() {
290 ctx.gesture.target_node = None;
291 return true;
292 }
293 return handled;
294 }
295 _ => {}
296 }
297 }
298 InputEvent::ExternalDrag(event) => match event {
299 ExternalDragEvent::Hover { point, paths, .. } => {
300 ctx.gesture.drag_session = Some(DragSessionState {
301 source_node: None,
302 source_identifier: None,
303 payload: DragSessionPayload::ExternalFiles(paths.clone()),
304 point: *point,
305 target_node: ctx
306 .gesture
307 .drag_session
308 .as_ref()
309 .and_then(|s| s.target_node),
310 target_identifier: ctx
311 .gesture
312 .drag_session
313 .as_ref()
314 .and_then(|s| s.target_identifier.clone()),
315 });
316 self.update_drag_target(ctx, *point);
317 return true;
318 }
319 ExternalDragEvent::Cancel => {
320 let point = ctx
321 .gesture
322 .drag_session
323 .as_ref()
324 .map(|session| session.point)
325 .unwrap_or(LayoutPoint::ZERO);
326 self.clear_drag_target(ctx, point);
327 ctx.gesture.drag_session = None;
328 return true;
329 }
330 ExternalDragEvent::Drop {
331 point,
332 paths,
333 modifiers,
334 } => {
335 ctx.gesture.drag_session = Some(DragSessionState {
336 source_node: None,
337 source_identifier: None,
338 payload: DragSessionPayload::ExternalFiles(paths.clone()),
339 point: *point,
340 target_node: ctx
341 .gesture
342 .drag_session
343 .as_ref()
344 .and_then(|s| s.target_node),
345 target_identifier: ctx
346 .gesture
347 .drag_session
348 .as_ref()
349 .and_then(|s| s.target_identifier.clone()),
350 });
351 self.update_drag_target(ctx, *point);
352 if let Some(target) = ctx
353 .gesture
354 .drag_session
355 .as_ref()
356 .and_then(|s| s.target_node)
357 {
358 let _ = self.dispatch_external_drop(
359 ctx,
360 target,
361 paths.clone(),
362 *point,
363 *modifiers,
364 );
365 }
366 self.clear_drag_target(ctx, *point);
367 ctx.gesture.drag_session = None;
368 return true;
369 }
370 },
371 _ => {}
372 }
373 false
374 }
375}
376
377impl GestureController {
378 fn path_for_node(&self, ctx: &ControllerContext, node_id: WidgetId) -> Vec<WidgetId> {
379 let mut path = Vec::new();
380 let mut curr = Some(node_id);
381 while let Some(id) = curr {
382 path.push(id);
383 curr = ctx.ir.nodes.get(&id).and_then(|node| node.parent);
384 }
385 path
386 }
387
388 fn is_descendant(&self, ctx: &ControllerContext, child: WidgetId, ancestor: WidgetId) -> bool {
389 let mut curr = Some(child);
390 while let Some(id) = curr {
391 if id == ancestor {
392 return true;
393 }
394 if let Some(node) = ctx.ir.nodes.get(&id) {
395 curr = node.parent;
396 } else {
397 break;
398 }
399 }
400 false
401 }
402
403 fn find_context_menu_owner(
404 &self,
405 ctx: &ControllerContext,
406 start_node: WidgetId,
407 ) -> Option<WidgetId> {
408 let mut current_id = Some(start_node);
409 while let Some(node_id) = current_id {
410 let Some(node) = ctx.ir.nodes.get(&node_id) else {
411 break;
412 };
413 if let Op::Semantics(semantics) = &node.op {
414 if semantics.context_menu && !semantics.disabled {
415 return Some(node_id);
416 }
417 }
418 current_id = node.parent;
419 }
420 None
421 }
422
423 fn dispatch_annotation_trigger(
424 &self,
425 ctx: &mut ControllerContext,
426 node_id: WidgetId,
427 annotation: &RichTextAnnotation,
428 trigger: ActionTrigger,
429 point: LayoutPoint,
430 ) -> bool {
431 let Some(action_entry) = annotation
432 .actions
433 .iter()
434 .find(|entry| entry.trigger == trigger)
435 else {
436 return false;
437 };
438 let Some(payload) = &action_entry.payload_data else {
439 return false;
440 };
441
442 let input = crate::input::scoped_action_input(
443 ctx.ir,
444 node_id,
445 ActionInput::Pointer {
446 x: point.x,
447 y: point.y,
448 delta_x: 0.0,
449 delta_y: 0.0,
450 },
451 );
452 ctx.dispatched_actions.push((
453 node_id,
454 ActionEnvelope {
455 id: ActionId::from_u128(action_entry.action_id),
456 payload: payload.clone(),
457 },
458 input,
459 ));
460 true
461 }
462
463 fn find_drag_payload(&self, ctx: &ControllerContext, start_node: WidgetId) -> Option<Vec<u8>> {
464 let mut current_id = Some(start_node);
465 while let Some(node_id) = current_id {
466 if let Some(node) = ctx.ir.nodes.get(&node_id) {
467 if let Op::Semantics(sem) = &node.op {
468 if let Some(p) = &sem.drag_payload {
469 return Some(p.clone());
470 }
471 }
472 current_id = node.parent;
473 } else {
474 break;
475 }
476 }
477 None
478 }
479
480 fn semantic_identifier(&self, ctx: &ControllerContext, start_node: WidgetId) -> Option<String> {
481 let mut current_id = Some(start_node);
482 while let Some(node_id) = current_id {
483 if let Some(node) = ctx.ir.nodes.get(&node_id) {
484 if let Op::Semantics(sem) = &node.op {
485 if let Some(identifier) = &sem.identifier {
486 return Some(identifier.clone());
487 }
488 }
489 current_id = node.parent;
490 } else {
491 break;
492 }
493 }
494 None
495 }
496
497 fn find_drop_target(&self, ctx: &ControllerContext, start_node: WidgetId) -> Option<WidgetId> {
498 let mut current_id = Some(start_node);
499 while let Some(node_id) = current_id {
500 if let Some(node) = ctx.ir.nodes.get(&node_id) {
501 if let Op::Semantics(sem) = &node.op {
502 if sem
503 .actions
504 .entries
505 .iter()
506 .any(|entry| entry.trigger == ActionTrigger::Drop)
507 {
508 return Some(node_id);
509 }
510 }
511 current_id = node.parent;
512 } else {
513 break;
514 }
515 }
516 None
517 }
518
519 fn update_drag_target(&self, ctx: &mut ControllerContext, point: LayoutPoint) {
520 let next_target =
521 crate::hit_test::hit_test_with_scroll(ctx.ir, ctx.layout, ctx.scroll, point)
522 .and_then(|hit| self.find_drop_target(ctx, hit));
523
524 let previous_target = ctx
525 .gesture
526 .drag_session
527 .as_ref()
528 .and_then(|s| s.target_node);
529 if previous_target == next_target {
530 return;
531 }
532
533 if let Some(previous) = previous_target {
534 self.dispatch_trigger(ctx, previous, ActionTrigger::DragLeave, point, None);
535 }
536 if let Some(next) = next_target {
537 self.dispatch_trigger(ctx, next, ActionTrigger::DragEnter, point, None);
538 }
539
540 let next_identifier = next_target.and_then(|id| self.semantic_identifier(ctx, id));
541 if let Some(session) = ctx.gesture.drag_session.as_mut() {
542 session.target_node = next_target;
543 session.target_identifier = next_identifier;
544 }
545 }
546
547 fn clear_drag_target(&self, ctx: &mut ControllerContext, point: LayoutPoint) {
548 if let Some(previous) = ctx
549 .gesture
550 .drag_session
551 .as_ref()
552 .and_then(|s| s.target_node)
553 {
554 self.dispatch_trigger(ctx, previous, ActionTrigger::DragLeave, point, None);
555 }
556 if let Some(session) = ctx.gesture.drag_session.as_mut() {
557 session.target_node = None;
558 session.target_identifier = None;
559 }
560 }
561
562 fn dispatch_internal_drop(
563 &self,
564 ctx: &mut ControllerContext,
565 target_node: WidgetId,
566 payload: Vec<u8>,
567 point: LayoutPoint,
568 modifiers: u8,
569 ) -> bool {
570 let mut current_id = Some(target_node);
571 while let Some(node_id) = current_id {
572 if let Some(node) = ctx.ir.nodes.get(&node_id) {
573 if let Op::Semantics(sem) = &node.op {
574 for entry in &sem.actions.entries {
575 if entry.trigger == ActionTrigger::Drop {
576 let envelope = ActionEnvelope {
577 id: ActionId::from_u128(entry.action_id),
578 payload: entry.payload_data.clone().unwrap_or_default(),
579 };
580
581 let input = crate::input::scoped_action_input(
582 ctx.ir,
583 node_id,
584 ActionInput::InternalDrop {
585 payload: payload.clone(),
586 x: point.x,
587 y: point.y,
588 modifiers,
589 },
590 );
591
592 ctx.dispatched_actions.push((node_id, envelope, input));
593 return true;
594 }
595 }
596 }
597 current_id = node.parent;
598 } else {
599 break;
600 }
601 }
602 false
603 }
604
605 fn dispatch_external_drop(
606 &self,
607 ctx: &mut ControllerContext,
608 target_node: WidgetId,
609 paths: Vec<String>,
610 point: LayoutPoint,
611 modifiers: u8,
612 ) -> bool {
613 let mut current_id = Some(target_node);
614 while let Some(node_id) = current_id {
615 if let Some(node) = ctx.ir.nodes.get(&node_id) {
616 if let Op::Semantics(sem) = &node.op {
617 for entry in &sem.actions.entries {
618 if entry.trigger == ActionTrigger::Drop {
619 let envelope = ActionEnvelope {
620 id: ActionId::from_u128(entry.action_id),
621 payload: entry.payload_data.clone().unwrap_or_default(),
622 };
623
624 let input = crate::input::scoped_action_input(
625 ctx.ir,
626 node_id,
627 ActionInput::Drop {
628 paths: paths.clone(),
629 x: point.x,
630 y: point.y,
631 modifiers,
632 },
633 );
634
635 ctx.dispatched_actions.push((node_id, envelope, input));
636 return true;
637 }
638 }
639 }
640 current_id = node.parent;
641 } else {
642 break;
643 }
644 }
645 false
646 }
647
648 fn dispatch_trigger(
649 &self,
650 ctx: &mut ControllerContext,
651 start_node: WidgetId,
652 trigger: ActionTrigger,
653 point: LayoutPoint,
654 delta: Option<LayoutPoint>,
655 ) -> bool {
656 let mut current_id = Some(start_node);
657 while let Some(node_id) = current_id {
658 if let Some(node) = ctx.ir.nodes.get(&node_id) {
659 if let Op::Semantics(sem) = &node.op {
660 for entry in &sem.actions.entries {
661 if entry.trigger == trigger {
662 let envelope = ActionEnvelope {
663 id: ActionId::from_u128(entry.action_id),
664 payload: entry.payload_data.clone().unwrap_or_default(),
665 };
666
667 let input = crate::input::scoped_action_input(
668 ctx.ir,
669 node_id,
670 ActionInput::Pointer {
671 x: point.x,
672 y: point.y,
673 delta_x: delta.map(|d| d.x).unwrap_or(0.0),
674 delta_y: delta.map(|d| d.y).unwrap_or(0.0),
675 },
676 );
677
678 ctx.dispatched_actions.push((node_id, envelope, input));
679 return true;
680 }
681 }
682 }
683 current_id = node.parent;
684 } else {
685 break;
686 }
687 }
688 false
689 }
690
691 fn handle_pan_update(&self, ctx: &mut ControllerContext, delta: LayoutPoint) -> bool {
692 if let Some(target) = ctx.gesture.target_node {
693 let mut current = Some(target);
694 while let Some(id) = current {
695 if let Some(node) = ctx.ir.nodes.get(&id) {
696 if let fission_ir::Op::Semantics(sem) = &node.op {
697 if sem.draggable {
698 return false;
699 }
700 }
701 if let fission_ir::Op::Layout(fission_ir::op::LayoutOp::Scroll {
702 direction,
703 ..
704 }) = &node.op
705 {
706 let current_offset = ctx.scroll.get_offset(id);
707 let move_val = match direction {
708 fission_ir::op::FlexDirection::Row => -delta.x,
709 fission_ir::op::FlexDirection::Column => -delta.y,
710 };
711
712 let mut new_offset = current_offset + move_val;
713
714 if let Some(geom) = ctx.layout.get_node_geometry(id) {
715 let max_offset =
716 if matches!(direction, fission_ir::op::FlexDirection::Row) {
717 (geom.content_size.width - geom.rect.width()).max(0.0)
718 } else {
719 (geom.content_size.height - geom.rect.height()).max(0.0)
720 };
721 new_offset = new_offset.clamp(0.0, max_offset);
722 }
723
724 ctx.scroll.set_offset(id, new_offset);
725 return true;
726 }
727 current = node.parent;
728 } else {
729 break;
730 }
731 }
732 }
733 false
734 }
735}