This commit is contained in:
2026-03-20 20:28:48 -04:00
parent f71e03317d
commit d79a3bb728
9 changed files with 702 additions and 52 deletions

226
lib/ui/src/interaction.rs Normal file
View 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))
);
}
}