use crate::layout::{HitTarget, InteractionTree}; use crate::scene::Point; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum PointerButton { Primary, Middle, } #[derive(Clone, Copy, Debug, PartialEq)] pub enum PointerEventKind { Move, Down { button: PointerButton }, Up { button: PointerButton }, Scroll { delta: Point }, LeaveWindow, } #[derive(Clone, Copy, Debug, PartialEq)] pub struct PointerEvent { pub pointer_id: u32, pub position: Point, pub kind: PointerEventKind, } impl PointerEvent { pub const fn new(pointer_id: u32, position: Point, kind: PointerEventKind) -> Self { Self { pointer_id, position, kind, } } } #[derive(Clone, Copy, Debug, PartialEq)] pub enum RoutedPointerEventKind { Enter, Leave, Move, Down { button: PointerButton }, Up { button: PointerButton }, Scroll { delta: Point }, } #[derive(Clone, Debug, PartialEq)] pub struct RoutedPointerEvent { pub kind: RoutedPointerEventKind, pub target: HitTarget, pub pointer_id: u32, pub position: Point, } #[derive(Default)] pub struct PointerRouter { hovered: Vec, pressed: Option>, } impl PointerRouter { pub fn new() -> Self { Self::default() } pub fn hovered_target(&self) -> Option<&HitTarget> { self.hovered.last() } pub fn hovered_targets(&self) -> &[HitTarget] { &self.hovered } pub fn pressed_target(&self) -> Option<&HitTarget> { self.pressed.as_ref().and_then(|targets| targets.last()) } pub fn pressed_targets(&self) -> &[HitTarget] { self.pressed.as_deref().unwrap_or(&[]) } pub fn route( &mut self, interaction_tree: &InteractionTree, event: PointerEvent, ) -> Vec { let hovered_targets = match event.kind { PointerEventKind::LeaveWindow => Vec::new(), _ => interaction_tree.hit_path(event.position), }; let hit_target = hovered_targets.last().cloned(); let mut routed = Vec::new(); let shared_depth = self .hovered .iter() .zip(hovered_targets.iter()) .take_while(|(previous, next)| previous == next) .count(); if shared_depth != self.hovered.len() || shared_depth != hovered_targets.len() { for previous in self.hovered[shared_depth..].iter().rev().cloned() { routed.push(RoutedPointerEvent { kind: RoutedPointerEventKind::Leave, target: previous, pointer_id: event.pointer_id, position: event.position, }); } for target in hovered_targets[shared_depth..].iter().cloned() { routed.push(RoutedPointerEvent { kind: RoutedPointerEventKind::Enter, target, pointer_id: event.pointer_id, position: event.position, }); } self.hovered = hovered_targets; } match event.kind { PointerEventKind::Move => { if let Some(target) = self .pressed .as_ref() .and_then(|targets| targets.last().cloned()) .or(hit_target.clone()) { routed.push(RoutedPointerEvent { kind: RoutedPointerEventKind::Move, target, pointer_id: event.pointer_id, position: event.position, }); } } PointerEventKind::Down { button } => { if let Some(target) = hit_target { self.pressed = Some(self.hovered.clone()); routed.push(RoutedPointerEvent { kind: RoutedPointerEventKind::Down { button }, target, pointer_id: event.pointer_id, position: event.position, }); } } PointerEventKind::Up { button } => { if let Some(target) = self .pressed .take() .and_then(|targets| targets.last().cloned()) .or(hit_target) { routed.push(RoutedPointerEvent { kind: RoutedPointerEventKind::Up { button }, target, pointer_id: event.pointer_id, position: event.position, }); } } PointerEventKind::Scroll { delta } => { if let Some(target) = hit_target { routed.push(RoutedPointerEvent { kind: RoutedPointerEventKind::Scroll { delta }, target, pointer_id: event.pointer_id, position: event.position, }); } } PointerEventKind::LeaveWindow => {} } routed } } #[cfg(test)] mod tests { use super::{ PointerButton, PointerEvent, PointerEventKind, PointerRouter, RoutedPointerEventKind, }; use crate::layout::{InteractionTree, LayoutNode, LayoutPath}; use crate::scene::{Point, Rect}; use crate::tree::{CursorIcon, ElementId}; fn interaction_tree() -> InteractionTree { InteractionTree { root: LayoutNode { path: LayoutPath::root(), element_id: None, rect: Rect::new(0.0, 0.0, 200.0, 120.0), corner_radius: 0.0, clip_rect: None, pointer_events: false, focusable: false, cursor: CursorIcon::Default, scroll_metrics: None, prepared_image: None, prepared_text: None, children: vec![ LayoutNode { path: LayoutPath::root().child(0), element_id: Some(ElementId::new(1)), rect: Rect::new(0.0, 0.0, 120.0, 120.0), corner_radius: 0.0, clip_rect: None, pointer_events: true, focusable: false, cursor: CursorIcon::Default, scroll_metrics: None, prepared_image: None, prepared_text: None, children: Vec::new(), }, LayoutNode { path: LayoutPath::root().child(1), element_id: Some(ElementId::new(2)), rect: Rect::new(80.0, 0.0, 120.0, 120.0), corner_radius: 0.0, clip_rect: None, pointer_events: true, focusable: false, cursor: CursorIcon::Default, scroll_metrics: None, prepared_image: None, prepared_text: None, children: Vec::new(), }, ], }, } } fn nested_interaction_tree() -> InteractionTree { InteractionTree { root: LayoutNode { path: LayoutPath::root(), element_id: None, rect: Rect::new(0.0, 0.0, 200.0, 120.0), corner_radius: 0.0, clip_rect: None, pointer_events: false, focusable: false, cursor: CursorIcon::Default, scroll_metrics: None, prepared_image: None, prepared_text: None, children: vec![LayoutNode { path: LayoutPath::root().child(0), element_id: Some(ElementId::new(1)), rect: Rect::new(0.0, 0.0, 160.0, 120.0), corner_radius: 0.0, clip_rect: None, pointer_events: true, focusable: false, cursor: CursorIcon::Default, scroll_metrics: None, prepared_image: None, prepared_text: None, children: vec![LayoutNode { path: LayoutPath::root().child(0).child(0), element_id: Some(ElementId::new(2)), rect: Rect::new(16.0, 16.0, 80.0, 40.0), corner_radius: 0.0, clip_rect: None, pointer_events: true, focusable: false, cursor: CursorIcon::Default, scroll_metrics: None, prepared_image: None, prepared_text: None, children: Vec::new(), }], }], }, } } #[test] fn router_emits_enter_and_move_for_new_hover_target() { let mut router = PointerRouter::new(); let routed = router.route( &interaction_tree(), PointerEvent::new(0, Point::new(20.0, 20.0), PointerEventKind::Move), ); assert_eq!(routed.len(), 2); assert_eq!(routed[0].kind, RoutedPointerEventKind::Enter); assert_eq!(routed[1].kind, RoutedPointerEventKind::Move); assert_eq!(routed[1].target.element_id, Some(ElementId::new(1))); } #[test] fn router_prefers_pressed_target_on_pointer_up() { let mut router = PointerRouter::new(); let tree = interaction_tree(); let _ = router.route( &tree, PointerEvent::new( 0, Point::new(20.0, 20.0), PointerEventKind::Down { button: PointerButton::Primary, }, ), ); let routed = router.route( &tree, PointerEvent::new( 0, Point::new(160.0, 20.0), PointerEventKind::Up { button: PointerButton::Primary, }, ), ); assert!(routed.iter().any(|event| matches!( event.kind, RoutedPointerEventKind::Up { button: PointerButton::Primary } ))); assert_eq!( routed.last().unwrap().target.element_id, Some(ElementId::new(1)) ); } #[test] fn router_prefers_pressed_target_on_pointer_move() { let mut router = PointerRouter::new(); let tree = interaction_tree(); let _ = router.route( &tree, PointerEvent::new( 0, Point::new(20.0, 20.0), PointerEventKind::Down { button: PointerButton::Primary, }, ), ); let routed = router.route( &tree, PointerEvent::new(0, Point::new(160.0, 20.0), PointerEventKind::Move), ); assert!( routed .iter() .any(|event| event.kind == RoutedPointerEventKind::Move) ); assert_eq!( routed.last().unwrap().target.element_id, Some(ElementId::new(1)) ); } #[test] fn router_routes_scroll_to_deepest_hover_target() { let mut router = PointerRouter::new(); let tree = nested_interaction_tree(); let _ = router.route( &tree, PointerEvent::new(0, Point::new(24.0, 24.0), PointerEventKind::Move), ); let routed = router.route( &tree, PointerEvent::new( 0, Point::new(24.0, 24.0), PointerEventKind::Scroll { delta: Point::new(0.0, 18.0), }, ), ); assert!(routed.iter().any(|event| { event.kind == RoutedPointerEventKind::Scroll { delta: Point::new(0.0, 18.0), } && event.target.element_id == Some(ElementId::new(2)) })); } #[test] fn router_tracks_hovered_ancestors_for_nested_hits() { let mut router = PointerRouter::new(); let routed = router.route( &nested_interaction_tree(), PointerEvent::new(0, Point::new(24.0, 24.0), PointerEventKind::Move), ); assert_eq!( routed .iter() .filter(|event| event.kind == RoutedPointerEventKind::Enter) .map(|event| event.target.element_id) .collect::>(), vec![Some(ElementId::new(1)), Some(ElementId::new(2))] ); assert_eq!( router .hovered_targets() .iter() .map(|target| target.element_id) .collect::>(), vec![Some(ElementId::new(1)), Some(ElementId::new(2))] ); assert_eq!( router.hovered_target().unwrap().element_id, Some(ElementId::new(2)) ); } #[test] fn router_leaves_only_nested_child_when_pointer_moves_to_parent() { let mut router = PointerRouter::new(); let tree = nested_interaction_tree(); let _ = router.route( &tree, PointerEvent::new(0, Point::new(24.0, 24.0), PointerEventKind::Move), ); let routed = router.route( &tree, PointerEvent::new(0, Point::new(140.0, 24.0), PointerEventKind::Move), ); assert_eq!( routed .iter() .filter(|event| event.kind == RoutedPointerEventKind::Leave) .map(|event| event.target.element_id) .collect::>(), vec![Some(ElementId::new(2))] ); assert_eq!( router .hovered_targets() .iter() .map(|target| target.element_id) .collect::>(), vec![Some(ElementId::new(1))] ); } }