Skip to main content

appscale_core/
events.rs

1//! Unified Event System — pointer, keyboard, and gesture events.
2//!
3//! Event flow: Native → Rust → React
4//! NOT: Native → React directly
5//!
6//! The dispatcher implements W3C-style capture → target → bubble propagation.
7//! The gesture recognizer synthesizes tap/pan/swipe from raw pointer sequences.
8
9use crate::tree::{NodeId, ShadowTree};
10use crate::layout::LayoutEngine;
11use rustc_hash::FxHashMap;
12use std::time::{Duration, Instant};
13
14// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
15// Event types
16// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
17
18/// Every input event in the framework.
19#[derive(Debug, Clone)]
20pub enum InputEvent {
21    PointerDown(PointerEvent),
22    PointerMove(PointerEvent),
23    PointerUp(PointerEvent),
24    PointerCancel(PointerEvent),
25    Scroll(ScrollEvent),
26    KeyDown(KeyboardEvent),
27    KeyUp(KeyboardEvent),
28    // Gestures (synthesized)
29    Tap { x: f32, y: f32, target: Option<NodeId> },
30    DoubleTap { x: f32, y: f32, target: Option<NodeId> },
31    LongPress { x: f32, y: f32, target: Option<NodeId> },
32    Pan { dx: f32, dy: f32, vx: f32, vy: f32, target: Option<NodeId>, ended: bool },
33    Swipe { direction: SwipeDirection, velocity: f32, target: Option<NodeId> },
34}
35
36#[derive(Debug, Clone)]
37pub struct PointerEvent {
38    pub pointer_id: u32,
39    pub pointer_type: PointerType,
40    pub screen_x: f32,
41    pub screen_y: f32,
42    pub pressure: f32,
43    pub buttons: u32,
44    pub modifiers: Modifiers,
45    pub timestamp: Instant,
46    pub target: Option<NodeId>,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum PointerType { Mouse, Touch, Pen }
51
52#[derive(Debug, Clone)]
53pub struct ScrollEvent {
54    pub delta_x: f32,
55    pub delta_y: f32,
56    pub modifiers: Modifiers,
57    pub target: Option<NodeId>,
58}
59
60#[derive(Debug, Clone)]
61pub struct KeyboardEvent {
62    pub code: String,
63    pub key: String,
64    pub modifiers: Modifiers,
65    pub is_repeat: bool,
66    pub target: Option<NodeId>,
67}
68
69#[derive(Debug, Clone, Copy, Default)]
70pub struct Modifiers {
71    pub shift: bool,
72    pub ctrl: bool,
73    pub alt: bool,
74    pub meta: bool,
75}
76
77#[derive(Debug, Clone, Copy)]
78pub enum SwipeDirection { Up, Down, Left, Right }
79
80// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
81// Event handlers and dispatch
82// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
83
84pub type HandlerFn = Box<dyn Fn(&InputEvent) -> HandlerResponse + Send + Sync>;
85
86#[derive(Debug, Clone, Copy, PartialEq)]
87pub enum HandlerResponse {
88    Continue,
89    StopPropagation,
90    PreventDefault,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
94pub enum EventPhase { Capture, Bubble }
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
97struct HandlerKey {
98    node_id: NodeId,
99    event_kind: EventKind,
100    phase: EventPhase,
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
104pub enum EventKind {
105    PointerDown, PointerMove, PointerUp, PointerCancel,
106    Scroll, KeyDown, KeyUp,
107    Tap, DoubleTap, LongPress, Pan, Swipe,
108}
109
110impl InputEvent {
111    pub fn kind(&self) -> EventKind {
112        match self {
113            InputEvent::PointerDown(_) => EventKind::PointerDown,
114            InputEvent::PointerMove(_) => EventKind::PointerMove,
115            InputEvent::PointerUp(_) => EventKind::PointerUp,
116            InputEvent::PointerCancel(_) => EventKind::PointerCancel,
117            InputEvent::Scroll(_) => EventKind::Scroll,
118            InputEvent::KeyDown(_) => EventKind::KeyDown,
119            InputEvent::KeyUp(_) => EventKind::KeyUp,
120            InputEvent::Tap { .. } => EventKind::Tap,
121            InputEvent::DoubleTap { .. } => EventKind::DoubleTap,
122            InputEvent::LongPress { .. } => EventKind::LongPress,
123            InputEvent::Pan { .. } => EventKind::Pan,
124            InputEvent::Swipe { .. } => EventKind::Swipe,
125        }
126    }
127
128    /// Get the screen position of this event (for hit testing).
129    pub fn screen_position(&self) -> Option<(f32, f32)> {
130        match self {
131            InputEvent::PointerDown(e) | InputEvent::PointerMove(e) |
132            InputEvent::PointerUp(e) | InputEvent::PointerCancel(e) => {
133                Some((e.screen_x, e.screen_y))
134            },
135            InputEvent::Tap { x, y, .. } | InputEvent::DoubleTap { x, y, .. } |
136            InputEvent::LongPress { x, y, .. } => Some((*x, *y)),
137            _ => None,
138        }
139    }
140}
141
142// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
143// Dispatcher
144// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
145
146pub struct EventResult {
147    pub propagation_stopped: bool,
148    pub default_prevented: bool,
149}
150
151pub struct EventDispatcher {
152    handlers: FxHashMap<HandlerKey, Vec<HandlerFn>>,
153    gesture: GestureRecognizer,
154}
155
156impl EventDispatcher {
157    pub fn new() -> Self {
158        Self {
159            handlers: FxHashMap::default(),
160            gesture: GestureRecognizer::new(),
161        }
162    }
163
164    /// Register a handler for a node + event kind + phase.
165    pub fn add_handler(
166        &mut self,
167        node_id: NodeId,
168        kind: EventKind,
169        phase: EventPhase,
170        handler: HandlerFn,
171    ) {
172        let key = HandlerKey { node_id, event_kind: kind, phase };
173        self.handlers.entry(key).or_default().push(handler);
174    }
175
176    /// Remove all handlers for a node (called when node is removed from tree).
177    pub fn remove_handlers_for(&mut self, node_id: NodeId) {
178        self.handlers.retain(|key, _| key.node_id != node_id);
179    }
180
181    /// Dispatch an event through the tree.
182    pub fn dispatch(
183        &mut self,
184        mut event: InputEvent,
185        layout: &LayoutEngine,
186        tree: &ShadowTree,
187    ) -> EventResult {
188        let mut result = EventResult {
189            propagation_stopped: false,
190            default_prevented: false,
191        };
192
193        // Hit test to find target
194        let target = if let Some((x, y)) = event.screen_position() {
195            layout.hit_test(x, y).first().copied()
196        } else {
197            None
198        };
199
200        // Set target on the event
201        self.set_target(&mut event, target);
202
203        let target = match target {
204            Some(t) => t,
205            None => return result,
206        };
207
208        // Build propagation path: [root, ..., parent, target]
209        let path = tree.ancestors(target);
210        let kind = event.kind();
211
212        // Capture phase (root → target)
213        for &node_id in &path {
214            if result.propagation_stopped { break; }
215            let key = HandlerKey { node_id, event_kind: kind, phase: EventPhase::Capture };
216            if let Some(handlers) = self.handlers.get(&key) {
217                for handler in handlers {
218                    match handler(&event) {
219                        HandlerResponse::StopPropagation => result.propagation_stopped = true,
220                        HandlerResponse::PreventDefault => result.default_prevented = true,
221                        HandlerResponse::Continue => {}
222                    }
223                    if result.propagation_stopped { break; }
224                }
225            }
226        }
227
228        // Bubble phase (target → root)
229        for &node_id in path.iter().rev() {
230            if result.propagation_stopped { break; }
231            let key = HandlerKey { node_id, event_kind: kind, phase: EventPhase::Bubble };
232            if let Some(handlers) = self.handlers.get(&key) {
233                for handler in handlers {
234                    match handler(&event) {
235                        HandlerResponse::StopPropagation => result.propagation_stopped = true,
236                        HandlerResponse::PreventDefault => result.default_prevented = true,
237                        HandlerResponse::Continue => {}
238                    }
239                    if result.propagation_stopped { break; }
240                }
241            }
242        }
243
244        // Feed to gesture recognizer (may produce tap/pan/swipe)
245        if let Some(gestures) = self.gesture.process(&event) {
246            for gesture in gestures {
247                self.dispatch(gesture, layout, tree);
248            }
249        }
250
251        result
252    }
253
254    fn set_target(&self, event: &mut InputEvent, target: Option<NodeId>) {
255        match event {
256            InputEvent::PointerDown(e) | InputEvent::PointerMove(e) |
257            InputEvent::PointerUp(e) | InputEvent::PointerCancel(e) => {
258                e.target = target;
259            },
260            InputEvent::Tap { target: t, .. } | InputEvent::DoubleTap { target: t, .. } |
261            InputEvent::LongPress { target: t, .. } | InputEvent::Pan { target: t, .. } |
262            InputEvent::Swipe { target: t, .. } => {
263                *t = target;
264            },
265            _ => {}
266        }
267    }
268}
269
270// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
271// Gesture recognizer
272// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
273
274const TAP_MAX_DURATION: Duration = Duration::from_millis(300);
275const TAP_MAX_DISTANCE: f32 = 10.0;
276const LONG_PRESS_MIN: Duration = Duration::from_millis(500);
277const PAN_THRESHOLD: f32 = 10.0;
278const SWIPE_MIN_VELOCITY: f32 = 300.0;
279
280struct PointerState {
281    start: Instant,
282    start_x: f32,
283    start_y: f32,
284    current_x: f32,
285    current_y: f32,
286    target: Option<NodeId>,
287    panning: bool,
288}
289
290struct GestureRecognizer {
291    pointers: FxHashMap<u32, PointerState>,
292}
293
294impl GestureRecognizer {
295    fn new() -> Self {
296        Self { pointers: FxHashMap::default() }
297    }
298
299    fn process(&mut self, event: &InputEvent) -> Option<Vec<InputEvent>> {
300        match event {
301            InputEvent::PointerDown(e) => {
302                self.pointers.insert(e.pointer_id, PointerState {
303                    start: e.timestamp,
304                    start_x: e.screen_x,
305                    start_y: e.screen_y,
306                    current_x: e.screen_x,
307                    current_y: e.screen_y,
308                    target: e.target,
309                    panning: false,
310                });
311                None
312            },
313            InputEvent::PointerMove(e) => {
314                let s = self.pointers.get_mut(&e.pointer_id)?;
315                s.current_x = e.screen_x;
316                s.current_y = e.screen_y;
317
318                let dx = s.current_x - s.start_x;
319                let dy = s.current_y - s.start_y;
320                let dist = (dx * dx + dy * dy).sqrt();
321
322                if !s.panning && dist > PAN_THRESHOLD {
323                    s.panning = true;
324                }
325
326                if s.panning {
327                    Some(vec![InputEvent::Pan {
328                        dx, dy, vx: 0.0, vy: 0.0,
329                        target: s.target, ended: false,
330                    }])
331                } else {
332                    None
333                }
334            },
335            InputEvent::PointerUp(e) => {
336                let s = self.pointers.remove(&e.pointer_id)?;
337                let dur = e.timestamp.duration_since(s.start);
338                let dx = e.screen_x - s.start_x;
339                let dy = e.screen_y - s.start_y;
340                let dist = (dx * dx + dy * dy).sqrt();
341
342                if s.panning {
343                    let velocity = dist / dur.as_secs_f32();
344                    let mut events = vec![InputEvent::Pan {
345                        dx, dy,
346                        vx: dx / dur.as_secs_f32(),
347                        vy: dy / dur.as_secs_f32(),
348                        target: s.target, ended: true,
349                    }];
350
351                    if velocity > SWIPE_MIN_VELOCITY {
352                        let dir = if dx.abs() > dy.abs() {
353                            if dx > 0.0 { SwipeDirection::Right } else { SwipeDirection::Left }
354                        } else {
355                            if dy > 0.0 { SwipeDirection::Down } else { SwipeDirection::Up }
356                        };
357                        events.push(InputEvent::Swipe {
358                            direction: dir, velocity, target: s.target,
359                        });
360                    }
361                    Some(events)
362                } else if dur < TAP_MAX_DURATION && dist < TAP_MAX_DISTANCE {
363                    Some(vec![InputEvent::Tap {
364                        x: e.screen_x, y: e.screen_y, target: s.target,
365                    }])
366                } else if dur >= LONG_PRESS_MIN && dist < TAP_MAX_DISTANCE {
367                    Some(vec![InputEvent::LongPress {
368                        x: e.screen_x, y: e.screen_y, target: s.target,
369                    }])
370                } else {
371                    None
372                }
373            },
374            _ => None,
375        }
376    }
377}
378
379// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
380// Tests
381// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
382
383#[cfg(test)]
384mod tests {
385    use super::*;
386    use crate::tree::{NodeId, ShadowTree};
387    use crate::layout::{LayoutEngine, LayoutStyle, Dimension};
388    use crate::platform::{ViewType};
389    use crate::platform::mock::MockPlatform;
390    use std::collections::HashMap;
391    use std::sync::{Arc, atomic::{AtomicU32, Ordering}};
392
393    /// Builds a simple tree: root (400x400) → child (200x200 at 0,0)
394    /// and returns (tree, layout, dispatcher)
395    fn setup_simple(platform: &Arc<MockPlatform>) -> (ShadowTree, LayoutEngine, EventDispatcher) {
396        let mut tree = ShadowTree::new();
397        let mut layout = LayoutEngine::new();
398
399        tree.create_node(NodeId(1), ViewType::Container, HashMap::new());
400        layout.create_node(NodeId(1), &LayoutStyle {
401            width: Dimension::Points(400.0),
402            height: Dimension::Points(400.0),
403            ..Default::default()
404        }).unwrap();
405
406        tree.create_node(NodeId(2), ViewType::Container, HashMap::new());
407        layout.create_node(NodeId(2), &LayoutStyle {
408            width: Dimension::Points(200.0),
409            height: Dimension::Points(200.0),
410            ..Default::default()
411        }).unwrap();
412
413        tree.set_root(NodeId(1));
414        layout.set_root(NodeId(1));
415        tree.append_child(NodeId(1), NodeId(2));
416        layout.set_children_from_tree(NodeId(1), &tree).unwrap();
417        layout.compute(&tree, 400.0, 400.0, &**platform).unwrap();
418
419        (tree, layout, EventDispatcher::new())
420    }
421
422    fn make_pointer_down(x: f32, y: f32) -> InputEvent {
423        InputEvent::PointerDown(PointerEvent {
424            pointer_id: 0,
425            pointer_type: PointerType::Mouse,
426            screen_x: x,
427            screen_y: y,
428            pressure: 1.0,
429            buttons: 1,
430            modifiers: Modifiers::default(),
431            timestamp: Instant::now(),
432            target: None,
433        })
434    }
435
436    #[test]
437    fn dispatch_bubble_phase() {
438        let platform = Arc::new(MockPlatform::new());
439        let (tree, layout, mut dispatcher) = setup_simple(&platform);
440
441        let counter = Arc::new(AtomicU32::new(0));
442        let c = counter.clone();
443        dispatcher.add_handler(NodeId(2), EventKind::PointerDown, EventPhase::Bubble,
444            Box::new(move |_| { c.fetch_add(1, Ordering::SeqCst); HandlerResponse::Continue }));
445
446        // Click inside child
447        let event = make_pointer_down(50.0, 50.0);
448        let result = dispatcher.dispatch(event, &layout, &tree);
449
450        assert!(!result.propagation_stopped);
451        assert_eq!(counter.load(Ordering::SeqCst), 1);
452    }
453
454    #[test]
455    fn dispatch_capture_phase() {
456        let platform = Arc::new(MockPlatform::new());
457        let (tree, layout, mut dispatcher) = setup_simple(&platform);
458
459        let order = Arc::new(std::sync::Mutex::new(Vec::new()));
460        let o1 = order.clone();
461        let o2 = order.clone();
462
463        // Root capture handler should fire first
464        dispatcher.add_handler(NodeId(1), EventKind::PointerDown, EventPhase::Capture,
465            Box::new(move |_| { o1.lock().unwrap().push(1); HandlerResponse::Continue }));
466        dispatcher.add_handler(NodeId(2), EventKind::PointerDown, EventPhase::Capture,
467            Box::new(move |_| { o2.lock().unwrap().push(2); HandlerResponse::Continue }));
468
469        let event = make_pointer_down(50.0, 50.0);
470        dispatcher.dispatch(event, &layout, &tree);
471
472        let fired = order.lock().unwrap().clone();
473        assert_eq!(fired, vec![1, 2], "Capture should fire root first, then target");
474    }
475
476    #[test]
477    fn dispatch_stop_propagation() {
478        let platform = Arc::new(MockPlatform::new());
479        let (tree, layout, mut dispatcher) = setup_simple(&platform);
480
481        let parent_hit = Arc::new(AtomicU32::new(0));
482        let p = parent_hit.clone();
483
484        // Child stops propagation in capture phase
485        dispatcher.add_handler(NodeId(2), EventKind::PointerDown, EventPhase::Capture,
486            Box::new(|_| HandlerResponse::StopPropagation));
487
488        // Parent bubble handler should NOT fire
489        dispatcher.add_handler(NodeId(1), EventKind::PointerDown, EventPhase::Bubble,
490            Box::new(move |_| { p.fetch_add(1, Ordering::SeqCst); HandlerResponse::Continue }));
491
492        let event = make_pointer_down(50.0, 50.0);
493        let result = dispatcher.dispatch(event, &layout, &tree);
494
495        assert!(result.propagation_stopped);
496        assert_eq!(parent_hit.load(Ordering::SeqCst), 0, "Bubble should not fire after stop");
497    }
498
499    #[test]
500    fn dispatch_prevent_default() {
501        let platform = Arc::new(MockPlatform::new());
502        let (tree, layout, mut dispatcher) = setup_simple(&platform);
503
504        dispatcher.add_handler(NodeId(2), EventKind::PointerDown, EventPhase::Bubble,
505            Box::new(|_| HandlerResponse::PreventDefault));
506
507        let event = make_pointer_down(50.0, 50.0);
508        let result = dispatcher.dispatch(event, &layout, &tree);
509
510        assert!(result.default_prevented);
511        assert!(!result.propagation_stopped);
512    }
513
514    #[test]
515    fn dispatch_misses_when_outside_bounds() {
516        let platform = Arc::new(MockPlatform::new());
517        let (tree, layout, mut dispatcher) = setup_simple(&platform);
518
519        let counter = Arc::new(AtomicU32::new(0));
520        let c = counter.clone();
521        dispatcher.add_handler(NodeId(2), EventKind::PointerDown, EventPhase::Bubble,
522            Box::new(move |_| { c.fetch_add(1, Ordering::SeqCst); HandlerResponse::Continue }));
523
524        // Click outside all nodes (500, 500 is beyond 400x400 root)
525        let event = make_pointer_down(500.0, 500.0);
526        let result = dispatcher.dispatch(event, &layout, &tree);
527
528        assert_eq!(counter.load(Ordering::SeqCst), 0, "Handler should not fire for miss");
529        assert!(!result.propagation_stopped);
530    }
531
532    #[test]
533    fn dispatch_full_capture_target_bubble() {
534        // Root(400x400) → Mid(300x300) → Leaf(100x100)
535        let platform = Arc::new(MockPlatform::new());
536        let mut tree = ShadowTree::new();
537        let mut layout = LayoutEngine::new();
538
539        for (id, w, h) in [(1, 400.0, 400.0), (2, 300.0, 300.0), (3, 100.0, 100.0)] {
540            tree.create_node(NodeId(id), ViewType::Container, HashMap::new());
541            layout.create_node(NodeId(id), &LayoutStyle {
542                width: Dimension::Points(w),
543                height: Dimension::Points(h),
544                ..Default::default()
545            }).unwrap();
546        }
547
548        tree.set_root(NodeId(1));
549        layout.set_root(NodeId(1));
550        tree.append_child(NodeId(1), NodeId(2));
551        tree.append_child(NodeId(2), NodeId(3));
552        layout.set_children_from_tree(NodeId(1), &tree).unwrap();
553        layout.set_children_from_tree(NodeId(2), &tree).unwrap();
554        layout.compute(&tree, 400.0, 400.0, &*platform).unwrap();
555
556        let mut dispatcher = EventDispatcher::new();
557        let order = Arc::new(std::sync::Mutex::new(Vec::new()));
558
559        for id in [1u64, 2, 3] {
560            let oc = order.clone();
561            dispatcher.add_handler(NodeId(id), EventKind::PointerDown, EventPhase::Capture,
562                Box::new(move |_| { oc.lock().unwrap().push((id, "cap")); HandlerResponse::Continue }));
563
564            let ob = order.clone();
565            dispatcher.add_handler(NodeId(id), EventKind::PointerDown, EventPhase::Bubble,
566                Box::new(move |_| { ob.lock().unwrap().push((id, "bub")); HandlerResponse::Continue }));
567        }
568
569        let event = make_pointer_down(50.0, 50.0);
570        dispatcher.dispatch(event, &layout, &tree);
571
572        let fired = order.lock().unwrap().clone();
573        // Capture: root → mid → leaf, then Bubble: leaf → mid → root
574        assert_eq!(fired, vec![
575            (1, "cap"), (2, "cap"), (3, "cap"),
576            (3, "bub"), (2, "bub"), (1, "bub"),
577        ]);
578    }
579
580    #[test]
581    fn remove_handlers_cleanup() {
582        let platform = Arc::new(MockPlatform::new());
583        let (tree, layout, mut dispatcher) = setup_simple(&platform);
584
585        let counter = Arc::new(AtomicU32::new(0));
586        let c = counter.clone();
587        dispatcher.add_handler(NodeId(2), EventKind::PointerDown, EventPhase::Bubble,
588            Box::new(move |_| { c.fetch_add(1, Ordering::SeqCst); HandlerResponse::Continue }));
589
590        dispatcher.remove_handlers_for(NodeId(2));
591
592        let event = make_pointer_down(50.0, 50.0);
593        dispatcher.dispatch(event, &layout, &tree);
594
595        assert_eq!(counter.load(Ordering::SeqCst), 0, "Handlers should be removed");
596    }
597
598    #[test]
599    fn gesture_tap_recognition() {
600        let platform = Arc::new(MockPlatform::new());
601        let (tree, layout, mut dispatcher) = setup_simple(&platform);
602
603        let tapped = Arc::new(AtomicU32::new(0));
604        let t = tapped.clone();
605        dispatcher.add_handler(NodeId(2), EventKind::Tap, EventPhase::Bubble,
606            Box::new(move |_| { t.fetch_add(1, Ordering::SeqCst); HandlerResponse::Continue }));
607
608        // Simulate quick pointer down + up at the same spot
609        let now = Instant::now();
610        let down = InputEvent::PointerDown(PointerEvent {
611            pointer_id: 1,
612            pointer_type: PointerType::Touch,
613            screen_x: 50.0, screen_y: 50.0,
614            pressure: 1.0, buttons: 1,
615            modifiers: Modifiers::default(),
616            timestamp: now, target: None,
617        });
618        let up = InputEvent::PointerUp(PointerEvent {
619            pointer_id: 1,
620            pointer_type: PointerType::Touch,
621            screen_x: 50.0, screen_y: 50.0,
622            pressure: 0.0, buttons: 0,
623            modifiers: Modifiers::default(),
624            timestamp: now + Duration::from_millis(50), target: None,
625        });
626
627        dispatcher.dispatch(down, &layout, &tree);
628        dispatcher.dispatch(up, &layout, &tree);
629
630        assert_eq!(tapped.load(Ordering::SeqCst), 1, "Tap gesture should fire");
631    }
632
633    #[test]
634    fn event_kind_mapping() {
635        assert_eq!(make_pointer_down(0.0, 0.0).kind(), EventKind::PointerDown);
636
637        let scroll = InputEvent::Scroll(ScrollEvent {
638            delta_x: 1.0, delta_y: 2.0,
639            modifiers: Modifiers::default(),
640            target: None,
641        });
642        assert_eq!(scroll.kind(), EventKind::Scroll);
643
644        let key = InputEvent::KeyDown(KeyboardEvent {
645            code: "KeyA".into(), key: "a".into(),
646            modifiers: Modifiers::default(), is_repeat: false, target: None,
647        });
648        assert_eq!(key.kind(), EventKind::KeyDown);
649    }
650
651    #[test]
652    fn screen_position_extraction() {
653        let pe = make_pointer_down(42.0, 99.0);
654        assert_eq!(pe.screen_position(), Some((42.0, 99.0)));
655
656        let tap = InputEvent::Tap { x: 10.0, y: 20.0, target: None };
657        assert_eq!(tap.screen_position(), Some((10.0, 20.0)));
658
659        let key = InputEvent::KeyDown(KeyboardEvent {
660            code: "Space".into(), key: " ".into(),
661            modifiers: Modifiers::default(), is_repeat: false, target: None,
662        });
663        assert_eq!(key.screen_position(), None);
664    }
665}