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 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 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 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 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 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 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}