Pointer
This commit is contained in:
226
lib/ui/src/interaction.rs
Normal file
226
lib/ui/src/interaction.rs
Normal file
@@ -0,0 +1,226 @@
|
||||
use crate::layout::{HitTarget, InteractionTree};
|
||||
use crate::scene::Point;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum PointerButton {
|
||||
Primary,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum PointerEventKind {
|
||||
Move,
|
||||
Down { button: PointerButton },
|
||||
Up { button: PointerButton },
|
||||
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, Eq, PartialEq)]
|
||||
pub enum RoutedPointerEventKind {
|
||||
Enter,
|
||||
Leave,
|
||||
Move,
|
||||
Down { button: PointerButton },
|
||||
Up { button: PointerButton },
|
||||
}
|
||||
|
||||
#[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: Option<HitTarget>,
|
||||
pressed: Option<HitTarget>,
|
||||
}
|
||||
|
||||
impl PointerRouter {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn hovered_target(&self) -> Option<&HitTarget> {
|
||||
self.hovered.as_ref()
|
||||
}
|
||||
|
||||
pub fn pressed_target(&self) -> Option<&HitTarget> {
|
||||
self.pressed.as_ref()
|
||||
}
|
||||
|
||||
pub fn route(
|
||||
&mut self,
|
||||
interaction_tree: &InteractionTree,
|
||||
event: PointerEvent,
|
||||
) -> Vec<RoutedPointerEvent> {
|
||||
let hit_target = match event.kind {
|
||||
PointerEventKind::LeaveWindow => None,
|
||||
_ => interaction_tree.hit_test(event.position),
|
||||
};
|
||||
|
||||
let mut routed = Vec::new();
|
||||
if self.hovered != hit_target {
|
||||
if let Some(previous) = self.hovered.take() {
|
||||
routed.push(RoutedPointerEvent {
|
||||
kind: RoutedPointerEventKind::Leave,
|
||||
target: previous,
|
||||
pointer_id: event.pointer_id,
|
||||
position: event.position,
|
||||
});
|
||||
}
|
||||
if let Some(target) = hit_target.clone() {
|
||||
routed.push(RoutedPointerEvent {
|
||||
kind: RoutedPointerEventKind::Enter,
|
||||
target: target.clone(),
|
||||
pointer_id: event.pointer_id,
|
||||
position: event.position,
|
||||
});
|
||||
self.hovered = Some(target);
|
||||
}
|
||||
}
|
||||
|
||||
match event.kind {
|
||||
PointerEventKind::Move => {
|
||||
if let Some(target) = hit_target {
|
||||
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(target.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().or(hit_target) {
|
||||
routed.push(RoutedPointerEvent {
|
||||
kind: RoutedPointerEventKind::Up { button },
|
||||
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::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),
|
||||
pointer_events: false,
|
||||
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),
|
||||
pointer_events: true,
|
||||
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),
|
||||
pointer_events: true,
|
||||
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))
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user