452 lines
14 KiB
Rust
452 lines
14 KiB
Rust
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<HitTarget>,
|
|
pressed: Option<Vec<HitTarget>>,
|
|
}
|
|
|
|
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<RoutedPointerEvent> {
|
|
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<_>>(),
|
|
vec![Some(ElementId::new(1)), Some(ElementId::new(2))]
|
|
);
|
|
assert_eq!(
|
|
router
|
|
.hovered_targets()
|
|
.iter()
|
|
.map(|target| target.element_id)
|
|
.collect::<Vec<_>>(),
|
|
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<_>>(),
|
|
vec![Some(ElementId::new(2))]
|
|
);
|
|
assert_eq!(
|
|
router
|
|
.hovered_targets()
|
|
.iter()
|
|
.map(|target| target.element_id)
|
|
.collect::<Vec<_>>(),
|
|
vec![Some(ElementId::new(1))]
|
|
);
|
|
}
|
|
}
|