Skip to main content

fission_core/input/
gesture.rs

1use super::{ControllerContext, InputController};
2use crate::event::{InputEvent, PointerEvent};
3use crate::{ActionEnvelope, ActionId, ActionInput};
4use fission_ir::{NodeId, Op, semantics::ActionTrigger};
5use fission_layout::LayoutPoint;
6
7pub struct GestureController;
8
9impl InputController for GestureController {
10    fn handle_event(&mut self, ctx: &mut ControllerContext, event: &InputEvent) -> bool {
11        match event {
12            InputEvent::Pointer(pe) => {
13                match pe {
14                    PointerEvent::Down { point, button } => {
15                        ctx.gesture.start_point = Some(*point);
16                        ctx.gesture.last_point = Some(*point);
17                        ctx.gesture.is_panning = false;
18                        ctx.gesture.pressed_button = Some(button.clone());
19                        
20                        if let Some(hit) = crate::hit_test::hit_test_with_scroll(ctx.ir, ctx.layout, ctx.scroll, *point) {
21                            ctx.gesture.target_node = Some(hit);
22                            ctx.gesture.dragging_payload = self.find_drag_payload(ctx, hit);
23                        } else {
24                            ctx.gesture.target_node = None;
25                            ctx.gesture.dragging_payload = None;
26                        }
27                    }
28                    PointerEvent::Move { point } => {
29                        if let Some(start) = ctx.gesture.start_point {
30                            let dx = point.x - start.x;
31                            let dy = point.y - start.y;
32                            let dist_sq = dx*dx + dy*dy;
33                            let threshold = 5.0 * 5.0; 
34                            
35                            if !ctx.gesture.is_panning && dist_sq > threshold {
36                                ctx.gesture.is_panning = true;
37                                // Dispatch DragStart now
38                                if let Some(target) = ctx.gesture.target_node {
39                                    self.dispatch_trigger(ctx, target, ActionTrigger::DragStart, *point, None);
40                                }
41                            }
42                            
43                            if ctx.gesture.is_panning {
44                                let last = ctx.gesture.last_point.unwrap_or(start);
45                                let delta = LayoutPoint { x: point.x - last.x, y: point.y - last.y };
46                                ctx.gesture.last_point = Some(*point);
47                                
48                                // Try dispatching DragUpdate
49                                let dispatched = if let Some(target) = ctx.gesture.target_node {
50                                    self.dispatch_trigger(ctx, target, ActionTrigger::DragUpdate, *point, Some(delta))
51                                } else { false };
52                                
53                                if dispatched {
54                                    return true;
55                                }
56                                
57                                // Fallback to Scroll Panning if DragUpdate not handled
58                                if self.handle_pan_update(ctx, delta) {
59                                    return true; 
60                                }
61                            }
62                        }
63                    }
64                    PointerEvent::Up { point, .. } => {
65                        let mut handled = false;
66                        let was_secondary = matches!(
67                            ctx.gesture.pressed_button,
68                            Some(crate::event::PointerButton::Secondary)
69                        );
70                        if ctx.gesture.is_panning {
71                            // Internal Drop
72                            if let Some(payload) = ctx.gesture.dragging_payload.take() {
73                                if let Some(up_hit) = crate::hit_test::hit_test_with_scroll(ctx.ir, ctx.layout, ctx.scroll, *point) {
74                                    if self.dispatch_internal_drop(ctx, up_hit, payload, *point) {
75                                        handled = true;
76                                    }
77                                }
78                            }
79
80                            if let Some(target) = ctx.gesture.target_node {
81                                self.dispatch_trigger(ctx, target, ActionTrigger::DragEnd, *point, None);
82                            }
83                            handled = true;
84                        } else if was_secondary {
85                            // Secondary click (right-click)
86                            if let Some(target) = ctx.gesture.target_node {
87                                if let Some(up_hit) = crate::hit_test::hit_test_with_scroll(ctx.ir, ctx.layout, ctx.scroll, *point) {
88                                    if up_hit == target || self.is_descendant(ctx, up_hit, target) || self.is_descendant(ctx, target, up_hit) {
89                                        if self.dispatch_trigger(ctx, target, ActionTrigger::SecondaryClick, *point, None) {
90                                            handled = true;
91                                        }
92                                    }
93                                }
94                            }
95                        } else {
96                            // Tap (primary click)
97                            if let Some(target) = ctx.gesture.target_node {
98                                if let Some(up_hit) = crate::hit_test::hit_test_with_scroll(ctx.ir, ctx.layout, ctx.scroll, *point) {
99                                    if up_hit == target || self.is_descendant(ctx, up_hit, target) || self.is_descendant(ctx, target, up_hit) {
100                                        if self.dispatch_trigger(ctx, target, ActionTrigger::Default, *point, None) {
101                                            handled = true;
102                                        }
103                                    }
104                                }
105                            }
106                        }
107
108                        ctx.gesture.start_point = None;
109                        ctx.gesture.is_panning = false;
110                        ctx.gesture.dragging_payload = None;
111                        ctx.gesture.pressed_button = None;
112                        return handled;
113                    }
114                    _ => {}
115                }
116            }
117            _ => {}
118        }
119        false
120    }
121}
122
123impl GestureController {
124    fn is_descendant(&self, ctx: &ControllerContext, child: NodeId, ancestor: NodeId) -> bool {
125        let mut curr = Some(child);
126        while let Some(id) = curr {
127            if id == ancestor { return true; }
128            if let Some(node) = ctx.ir.nodes.get(&id) {
129                curr = node.parent;
130            } else { break; }
131        }
132        false
133    }
134
135    fn find_drag_payload(&self, ctx: &ControllerContext, start_node: NodeId) -> Option<Vec<u8>> {
136        let mut current_id = Some(start_node);
137        while let Some(node_id) = current_id {
138            if let Some(node) = ctx.ir.nodes.get(&node_id) {
139                if let Op::Semantics(sem) = &node.op {
140                    if let Some(p) = &sem.drag_payload {
141                        return Some(p.clone());
142                    }
143                }
144                current_id = node.parent;
145            } else { break; }
146        }
147        None
148    }
149
150    fn dispatch_internal_drop(&self, ctx: &mut ControllerContext, target_node: NodeId, payload: Vec<u8>, point: LayoutPoint) -> bool {
151        let mut current_id = Some(target_node);
152        while let Some(node_id) = current_id {
153            if let Some(node) = ctx.ir.nodes.get(&node_id) {
154                if let Op::Semantics(sem) = &node.op {
155                    for entry in &sem.actions.entries {
156                        if entry.trigger == ActionTrigger::Drop {
157                            let envelope = ActionEnvelope {
158                                id: ActionId::from_u128(entry.action_id),
159                                payload: entry.payload_data.clone().unwrap_or_default(),
160                            };
161                            
162                            let input = ActionInput::InternalDrop { 
163                                payload: payload.clone(),
164                                x: point.x, 
165                                y: point.y, 
166                            };
167                            
168                            ctx.dispatched_actions.push((node_id, envelope, input));
169                            return true;
170                        }
171                    }
172                }
173                current_id = node.parent;
174            } else { break; }
175        }
176        false
177    }
178
179    fn dispatch_trigger(&self, ctx: &mut ControllerContext, start_node: NodeId, trigger: ActionTrigger, point: LayoutPoint, delta: Option<LayoutPoint>) -> bool {
180        let mut current_id = Some(start_node);
181        while let Some(node_id) = current_id {
182            if let Some(node) = ctx.ir.nodes.get(&node_id) {
183                if let Op::Semantics(sem) = &node.op {
184                    for entry in &sem.actions.entries {
185                        if entry.trigger == trigger {
186                            let envelope = ActionEnvelope {
187                                id: ActionId::from_u128(entry.action_id),
188                                payload: entry.payload_data.clone().unwrap_or_default(),
189                            };
190                            
191                            let input = ActionInput::Pointer { 
192                                x: point.x, 
193                                y: point.y, 
194                                delta_x: delta.map(|d| d.x).unwrap_or(0.0), 
195                                delta_y: delta.map(|d| d.y).unwrap_or(0.0), 
196                            };
197                            
198                            ctx.dispatched_actions.push((node_id, envelope, input));
199                            return true; 
200                        }
201                    }
202                }
203                current_id = node.parent;
204            } else { break; }
205        }
206        false
207    }
208
209    fn handle_pan_update(&self, ctx: &mut ControllerContext, delta: LayoutPoint) -> bool {
210        if let Some(target) = ctx.gesture.target_node {
211            let mut current = Some(target);
212            while let Some(id) = current {
213                if let Some(node) = ctx.ir.nodes.get(&id) {
214                    if let fission_ir::Op::Semantics(sem) = &node.op {
215                        if sem.draggable {
216                            return false; 
217                        }
218                    }
219                    if let fission_ir::Op::Layout(fission_ir::op::LayoutOp::Scroll { direction, .. }) = &node.op {
220                        let current_offset = ctx.scroll.get_offset(id);
221                        let move_val = match direction {
222                            fission_ir::op::FlexDirection::Row => -delta.x,
223                            fission_ir::op::FlexDirection::Column => -delta.y,
224                        };
225                        
226                        let mut new_offset = current_offset + move_val;
227                        
228                        if let Some(geom) = ctx.layout.get_node_geometry(id) {
229                            let max_offset = if matches!(direction, fission_ir::op::FlexDirection::Row) {
230                                (geom.content_size.width - geom.rect.width()).max(0.0)
231                            } else {
232                                (geom.content_size.height - geom.rect.height()).max(0.0)
233                            };
234                            new_offset = new_offset.clamp(0.0, max_offset);
235                        }
236                        
237                        ctx.scroll.set_offset(id, new_offset);
238                        return true;
239                    }
240                    current = node.parent;
241                } else { break; }
242            }
243        }
244        false
245    }
246}