Skip to main content

fission_core/input/
gesture.rs

1use super::{ControllerContext, InputController};
2use crate::event::{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};
8use fission_ir::op::RichTextAnnotation;
9use fission_ir::{semantics::ActionTrigger, NodeId, Op};
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                                // Dispatch DragStart now
94                                if let Some(target) = ctx.gesture.target_node {
95                                    self.dispatch_trigger(
96                                        ctx,
97                                        target,
98                                        ActionTrigger::DragStart,
99                                        *point,
100                                        None,
101                                    );
102                                }
103                            }
104
105                            if ctx.gesture.is_panning {
106                                let last = ctx.gesture.last_point.unwrap_or(start);
107                                let delta = LayoutPoint {
108                                    x: point.x - last.x,
109                                    y: point.y - last.y,
110                                };
111                                ctx.gesture.last_point = Some(*point);
112
113                                // Try dispatching DragUpdate
114                                let dispatched = if let Some(target) = ctx.gesture.target_node {
115                                    self.dispatch_trigger(
116                                        ctx,
117                                        target,
118                                        ActionTrigger::DragUpdate,
119                                        *point,
120                                        Some(delta),
121                                    )
122                                } else {
123                                    false
124                                };
125
126                                if dispatched {
127                                    return true;
128                                }
129
130                                // Fallback to Scroll Panning if DragUpdate not handled
131                                if self.handle_pan_update(ctx, delta) {
132                                    return true;
133                                }
134                            }
135                        }
136                    }
137                    PointerEvent::Up { point, .. } => {
138                        let scrollbar_drag = ctx.gesture.scrollbar_drag.take();
139                        let mut handled = false;
140                        let was_secondary = matches!(
141                            ctx.gesture.pressed_button,
142                            Some(crate::event::PointerButton::Secondary)
143                        );
144                        if ctx.gesture.is_panning {
145                            // Internal Drop
146                            if let Some(payload) = ctx.gesture.dragging_payload.take() {
147                                if let Some(up_hit) = crate::hit_test::hit_test_with_scroll(
148                                    ctx.ir, ctx.layout, ctx.scroll, *point,
149                                ) {
150                                    let _ =
151                                        self.dispatch_internal_drop(ctx, up_hit, payload, *point);
152                                }
153                            }
154
155                            if let Some(target) = ctx.gesture.target_node {
156                                self.dispatch_trigger(
157                                    ctx,
158                                    target,
159                                    ActionTrigger::DragEnd,
160                                    *point,
161                                    None,
162                                );
163                            }
164                            handled = true;
165                        } else if was_secondary {
166                            // Secondary click (right-click)
167                            if let Some(target) = ctx.gesture.target_node {
168                                if let Some(up_hit) = crate::hit_test::hit_test_with_scroll(
169                                    ctx.ir, ctx.layout, ctx.scroll, *point,
170                                ) {
171                                    if up_hit == target
172                                        || self.is_descendant(ctx, up_hit, target)
173                                        || self.is_descendant(ctx, target, up_hit)
174                                    {
175                                        let rich_text_path = self.path_from_node(ctx, up_hit);
176                                        if let Some((annotation_node_id, annotation)) =
177                                            crate::input::hover::resolve_rich_text_annotation_at_point(
178                                                ctx,
179                                                &rich_text_path,
180                                                *point,
181                                            )
182                                        {
183                                            handled = self.dispatch_annotation_trigger(
184                                                ctx,
185                                                annotation_node_id,
186                                                &annotation,
187                                                ActionTrigger::SecondaryClick,
188                                                *point,
189                                            );
190                                        }
191
192                                        if !handled
193                                            && self.dispatch_trigger(
194                                                ctx,
195                                                target,
196                                                ActionTrigger::SecondaryClick,
197                                                *point,
198                                                None,
199                                            )
200                                        {
201                                            handled = true;
202                                        }
203                                    }
204                                }
205                            }
206                        } else {
207                            // Tap (primary click)
208                            if let Some(target) = ctx.gesture.target_node {
209                                if let Some(up_hit) = crate::hit_test::hit_test_with_scroll(
210                                    ctx.ir, ctx.layout, ctx.scroll, *point,
211                                ) {
212                                    if up_hit == target
213                                        || self.is_descendant(ctx, up_hit, target)
214                                        || self.is_descendant(ctx, target, up_hit)
215                                    {
216                                        let rich_text_path = self.path_from_node(ctx, up_hit);
217                                        if let Some((annotation_node_id, annotation)) =
218                                            crate::input::hover::resolve_rich_text_annotation_at_point(
219                                                ctx,
220                                                &rich_text_path,
221                                                *point,
222                                            )
223                                        {
224                                            handled = self.dispatch_annotation_trigger(
225                                                ctx,
226                                                annotation_node_id,
227                                                &annotation,
228                                                ActionTrigger::Default,
229                                                *point,
230                                            );
231                                        }
232
233                                        if !handled
234                                            && self.dispatch_trigger(
235                                                ctx,
236                                                target,
237                                                ActionTrigger::Default,
238                                                *point,
239                                                None,
240                                            )
241                                        {
242                                            handled = true;
243                                        }
244                                    }
245                                }
246                            }
247                        }
248
249                        ctx.gesture.start_point = None;
250                        ctx.gesture.is_panning = false;
251                        ctx.gesture.dragging_payload = None;
252                        ctx.gesture.pressed_button = None;
253                        if scrollbar_drag.is_some() {
254                            ctx.gesture.target_node = None;
255                            return true;
256                        }
257                        return handled;
258                    }
259                    _ => {}
260                }
261            }
262            _ => {}
263        }
264        false
265    }
266}
267
268impl GestureController {
269    fn path_from_node(&self, ctx: &ControllerContext, node_id: NodeId) -> Vec<NodeId> {
270        let mut path = Vec::new();
271        let mut curr = Some(node_id);
272        while let Some(id) = curr {
273            path.push(id);
274            curr = ctx.ir.nodes.get(&id).and_then(|node| node.parent);
275        }
276        path
277    }
278
279    fn is_descendant(&self, ctx: &ControllerContext, child: NodeId, ancestor: NodeId) -> bool {
280        let mut curr = Some(child);
281        while let Some(id) = curr {
282            if id == ancestor {
283                return true;
284            }
285            if let Some(node) = ctx.ir.nodes.get(&id) {
286                curr = node.parent;
287            } else {
288                break;
289            }
290        }
291        false
292    }
293
294    fn dispatch_annotation_trigger(
295        &self,
296        ctx: &mut ControllerContext,
297        node_id: NodeId,
298        annotation: &RichTextAnnotation,
299        trigger: ActionTrigger,
300        point: LayoutPoint,
301    ) -> bool {
302        let Some(action_entry) = annotation
303            .actions
304            .iter()
305            .find(|entry| entry.trigger == trigger)
306        else {
307            return false;
308        };
309        let Some(payload) = &action_entry.payload_data else {
310            return false;
311        };
312
313        let input = crate::input::scoped_action_input(
314            ctx.ir,
315            node_id,
316            ActionInput::Pointer {
317                x: point.x,
318                y: point.y,
319                delta_x: 0.0,
320                delta_y: 0.0,
321            },
322        );
323        ctx.dispatched_actions.push((
324            node_id,
325            ActionEnvelope {
326                id: ActionId::from_u128(action_entry.action_id),
327                payload: payload.clone(),
328            },
329            input,
330        ));
331        true
332    }
333
334    fn find_drag_payload(&self, ctx: &ControllerContext, start_node: NodeId) -> Option<Vec<u8>> {
335        let mut current_id = Some(start_node);
336        while let Some(node_id) = current_id {
337            if let Some(node) = ctx.ir.nodes.get(&node_id) {
338                if let Op::Semantics(sem) = &node.op {
339                    if let Some(p) = &sem.drag_payload {
340                        return Some(p.clone());
341                    }
342                }
343                current_id = node.parent;
344            } else {
345                break;
346            }
347        }
348        None
349    }
350
351    fn dispatch_internal_drop(
352        &self,
353        ctx: &mut ControllerContext,
354        target_node: NodeId,
355        payload: Vec<u8>,
356        point: LayoutPoint,
357    ) -> bool {
358        let mut current_id = Some(target_node);
359        while let Some(node_id) = current_id {
360            if let Some(node) = ctx.ir.nodes.get(&node_id) {
361                if let Op::Semantics(sem) = &node.op {
362                    for entry in &sem.actions.entries {
363                        if entry.trigger == ActionTrigger::Drop {
364                            let envelope = ActionEnvelope {
365                                id: ActionId::from_u128(entry.action_id),
366                                payload: entry.payload_data.clone().unwrap_or_default(),
367                            };
368
369                            let input = crate::input::scoped_action_input(
370                                ctx.ir,
371                                node_id,
372                                ActionInput::InternalDrop {
373                                    payload: payload.clone(),
374                                    x: point.x,
375                                    y: point.y,
376                                },
377                            );
378
379                            ctx.dispatched_actions.push((node_id, envelope, input));
380                            return true;
381                        }
382                    }
383                }
384                current_id = node.parent;
385            } else {
386                break;
387            }
388        }
389        false
390    }
391
392    fn dispatch_trigger(
393        &self,
394        ctx: &mut ControllerContext,
395        start_node: NodeId,
396        trigger: ActionTrigger,
397        point: LayoutPoint,
398        delta: Option<LayoutPoint>,
399    ) -> bool {
400        let mut current_id = Some(start_node);
401        while let Some(node_id) = current_id {
402            if let Some(node) = ctx.ir.nodes.get(&node_id) {
403                if let Op::Semantics(sem) = &node.op {
404                    for entry in &sem.actions.entries {
405                        if entry.trigger == trigger {
406                            let envelope = ActionEnvelope {
407                                id: ActionId::from_u128(entry.action_id),
408                                payload: entry.payload_data.clone().unwrap_or_default(),
409                            };
410
411                            let input = crate::input::scoped_action_input(
412                                ctx.ir,
413                                node_id,
414                                ActionInput::Pointer {
415                                    x: point.x,
416                                    y: point.y,
417                                    delta_x: delta.map(|d| d.x).unwrap_or(0.0),
418                                    delta_y: delta.map(|d| d.y).unwrap_or(0.0),
419                                },
420                            );
421
422                            ctx.dispatched_actions.push((node_id, envelope, input));
423                            return true;
424                        }
425                    }
426                }
427                current_id = node.parent;
428            } else {
429                break;
430            }
431        }
432        false
433    }
434
435    fn handle_pan_update(&self, ctx: &mut ControllerContext, delta: LayoutPoint) -> bool {
436        if let Some(target) = ctx.gesture.target_node {
437            let mut current = Some(target);
438            while let Some(id) = current {
439                if let Some(node) = ctx.ir.nodes.get(&id) {
440                    if let fission_ir::Op::Semantics(sem) = &node.op {
441                        if sem.draggable {
442                            return false;
443                        }
444                    }
445                    if let fission_ir::Op::Layout(fission_ir::op::LayoutOp::Scroll {
446                        direction,
447                        ..
448                    }) = &node.op
449                    {
450                        let current_offset = ctx.scroll.get_offset(id);
451                        let move_val = match direction {
452                            fission_ir::op::FlexDirection::Row => -delta.x,
453                            fission_ir::op::FlexDirection::Column => -delta.y,
454                        };
455
456                        let mut new_offset = current_offset + move_val;
457
458                        if let Some(geom) = ctx.layout.get_node_geometry(id) {
459                            let max_offset =
460                                if matches!(direction, fission_ir::op::FlexDirection::Row) {
461                                    (geom.content_size.width - geom.rect.width()).max(0.0)
462                                } else {
463                                    (geom.content_size.height - geom.rect.height()).max(0.0)
464                                };
465                            new_offset = new_offset.clamp(0.0, max_offset);
466                        }
467
468                        ctx.scroll.set_offset(id, new_offset);
469                        return true;
470                    }
471                    current = node.parent;
472                } else {
473                    break;
474                }
475            }
476        }
477        false
478    }
479}