use lieui_geom::Point;
use crate::event::PointerId;
use crate::track::{Layer, NodeId, Track, Visibility};
use crate::transform::Affine;
const LAYER_TOP_DOWN: [Layer; 6] = [
Layer::DragPreview,
Layer::Modal,
Layer::Tooltip,
Layer::Popup,
Layer::Overlay,
Layer::Content,
];
pub fn hit_path(track: &Track, p: Point) -> Vec<NodeId> {
for layer in LAYER_TOP_DOWN {
let roots: Vec<NodeId> = track.roots_of(layer).map(|r| r.node).collect();
for root in roots.into_iter().rev() {
let Some(r) = track.roots().iter().find(|r| r.node == root) else {
continue;
};
if !r.opts.hit_test_visible {
continue;
}
let mut path = Vec::new();
if descend(track, root, p, Affine::IDENTITY, &mut path) {
return path;
}
if r.opts.blocks_below {
return vec![root];
}
}
}
Vec::new()
}
pub fn hit_test(track: &Track, p: Point) -> Option<NodeId> {
hit_path(track, p).last().copied()
}
pub fn path_to(track: &Track, id: NodeId) -> Vec<NodeId> {
if !track.contains(id) {
return Vec::new();
}
let mut path: Vec<NodeId> = track.ancestors(id).collect();
path.reverse();
path.push(id);
path
}
pub fn hit_path_for(track: &Track, pointer: PointerId, p: Point) -> Vec<NodeId> {
match track.captured_by(pointer) {
Some(id) if track.contains(id) => path_to(track, id),
_ => hit_path(track, p),
}
}
fn descend(track: &Track, id: NodeId, p: Point, to_parent: Affine, out: &mut Vec<NodeId>) -> bool {
let Some(n) = track.get(id) else {
return false;
};
if n.visibility != Visibility::Visible {
return false;
}
let rect = n.rect();
let to_local = match n.transform.matrix(rect).inverse() {
Some(inv) => inv.then(to_parent),
None => return false,
};
let q = to_local.apply(p);
if !rect.contains(q) {
return false;
}
if let Some(clip) = n.clip
&& !clip.contains(q)
{
return false;
}
if !n.hit_test_visible {
return false;
}
out.push(id);
for c in n.children.iter().rev() {
if descend(track, *c, p, to_local, out) {
return true;
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cmd::{Cmd, apply_cmds};
use crate::layout::{layout, rect_of};
use crate::track::{Kind, Layer};
use lieui_geom::{Rect, Size};
const WINDOW: Size = Size::new(300.0, 100.0);
fn fixed(t: &mut Track, w: f32, h: f32) -> NodeId {
let id = t.create(Kind::Box, None);
t.get_mut(id).unwrap().layout.dim = [w, h];
id
}
fn relayout(t: &mut Track) {
layout(t, WINDOW);
}
fn set_visible(t: &mut Track, id: NodeId, v: Visibility) {
apply_cmds(
t,
&[Cmd::SetVisibility {
id,
visibility: v,
}],
);
}
fn setup() -> (Track, NodeId, Vec<NodeId>) {
let mut t = Track::new();
let root = t.create(Kind::Box, None);
t.get_mut(root).unwrap().layout.flex_direction = lieui_layout::FlexDirection::Row;
t.add_root(Layer::Content, None, root);
let kids: Vec<NodeId> = (0..3).map(|_| fixed(&mut t, 100.0, 100.0)).collect();
for k in &kids {
t.append_child(root, *k);
}
relayout(&mut t);
(t, root, kids)
}
#[test]
fn hits_the_child_under_the_point() {
let (t, root, kids) = setup();
assert_eq!(rect_of(&t, kids[1]), Rect::new(100.0, 0.0, 100.0, 100.0));
let p = Point::new(150.0, 50.0);
assert_eq!(hit_path(&t, p), vec![root, kids[1]]);
assert_eq!(hit_test(&t, p), Some(kids[1]));
}
#[test]
fn background_hit_returns_empty() {
let (t, _, _) = setup();
assert!(hit_path(&t, Point::new(-10.0, -10.0)).is_empty());
}
#[test]
fn collapsed_child_shifts_siblings_and_is_unhittable() {
let (mut t, root, kids) = setup();
set_visible(&mut t, kids[1], Visibility::Collapsed);
relayout(&mut t);
assert_eq!(rect_of(&t, kids[2]).x, 100.0, "兄弟补位");
assert_eq!(hit_test(&t, Point::new(150.0, 50.0)), Some(kids[2]));
set_visible(&mut t, kids[2], Visibility::Collapsed);
relayout(&mut t);
let path = hit_path(&t, Point::new(250.0, 50.0));
assert_eq!(path, vec![root], "空白处命中层根");
assert!(!path.contains(&kids[2]));
}
#[test]
fn hidden_occupies_space_but_is_not_hittable() {
let (mut t, root, kids) = setup();
set_visible(&mut t, kids[0], Visibility::Hidden);
relayout(&mut t);
assert_eq!(rect_of(&t, kids[0]), Rect::new(0.0, 0.0, 100.0, 100.0));
assert_eq!(rect_of(&t, kids[1]).x, 100.0);
assert_eq!(hit_test(&t, Point::new(50.0, 50.0)), Some(root));
}
#[test]
fn hit_test_visible_false_makes_the_subtree_transparent() {
let (mut t, root, kids) = setup();
let inner = fixed(&mut t, 50.0, 50.0);
t.append_child(kids[0], inner);
relayout(&mut t);
assert_eq!(hit_test(&t, Point::new(10.0, 10.0)), Some(inner));
t.get_mut(kids[0]).unwrap().hit_test_visible = false;
let path = hit_path(&t, Point::new(10.0, 10.0));
assert!(!path.contains(&kids[0]), "整棵子树穿透");
assert!(!path.contains(&inner));
assert_eq!(path, vec![root], "只剩层根");
}
#[test]
fn clip_rejects_outside_points() {
let (mut t, root, kids) = setup();
t.get_mut(kids[1]).unwrap().clip = Some(Rect::new(100.0, 0.0, 10.0, 10.0));
let path = hit_path(&t, Point::new(150.0, 50.0));
assert!(!path.contains(&kids[1]), "clip 之外不命中");
assert_eq!(path, vec![root]);
}
#[test]
fn transform_shrinks_the_hit_region() {
let (mut t, root, kids) = setup();
t.get_mut(kids[1]).unwrap().transform.scale = (0.5, 0.5);
assert_eq!(hit_test(&t, Point::new(150.0, 50.0)), Some(kids[1]), "中心仍命中");
let path = hit_path(&t, Point::new(103.0, 5.0));
assert!(!path.contains(&kids[1]));
assert_eq!(path, vec![root]);
}
#[test]
fn transform_translate_moves_the_hit_region() {
let (mut t, _, kids) = setup();
t.get_mut(kids[0]).unwrap().transform.translate = (50.0, 0.0);
assert_eq!(hit_test(&t, Point::new(60.0, 50.0)), Some(kids[0]), "新位置命中");
assert_ne!(hit_test(&t, Point::new(20.0, 50.0)), Some(kids[0]), "原位不再命中");
assert_eq!(hit_test(&t, Point::new(150.0, 50.0)), Some(kids[1]));
}
#[test]
fn parent_transform_applies_to_children() {
let (mut t, _, kids) = setup();
t.get_mut(kids[0]).unwrap().transform.scale = (2.0, 2.0);
let child = fixed(&mut t, 50.0, 50.0);
t.append_child(kids[0], child);
relayout(&mut t);
assert_eq!(rect_of(&t, child), Rect::new(0.0, 0.0, 50.0, 50.0));
assert_eq!(hit_test(&t, Point::new(30.0, 30.0)), Some(child));
assert_eq!(hit_test(&t, Point::new(90.0, 90.0)), Some(kids[0]));
}
#[test]
fn degenerate_transform_is_unhittable() {
let (mut t, root, kids) = setup();
t.get_mut(kids[1]).unwrap().transform.scale = (0.0, 1.0);
assert_ne!(hit_test(&t, Point::new(150.0, 50.0)), Some(kids[1]));
assert_eq!(hit_test(&t, Point::new(150.0, 50.0)), Some(root));
}
#[test]
fn topmost_layer_wins() {
let (mut t, _, _) = setup();
let popup = fixed(&mut t, 100.0, 100.0);
t.add_root(Layer::Popup, None, popup);
relayout(&mut t);
assert_eq!(hit_path(&t, Point::new(50.0, 50.0)), vec![popup]);
}
#[test]
fn modal_absorbs_everything_below() {
let (mut t, content, _kids) = setup();
let modal = fixed(&mut t, 100.0, 100.0); t.add_root(Layer::Modal, None, modal);
relayout(&mut t);
assert_eq!(hit_path(&t, Point::new(50.0, 50.0)), vec![modal]);
let path = hit_path(&t, Point::new(250.0, 50.0));
assert_eq!(path, vec![modal]);
assert!(!path.contains(&content));
}
#[test]
fn overlay_layer_passes_through_by_default() {
let (mut t, content, _) = setup();
let overlay = fixed(&mut t, 300.0, 100.0);
t.add_root(Layer::Overlay, None, overlay);
relayout(&mut t);
let path = hit_path(&t, Point::new(50.0, 50.0));
assert!(!path.contains(&overlay), "水印层默认命中穿透");
assert!(path.contains(&content));
}
#[test]
fn later_root_in_the_same_layer_is_on_top() {
let mut t = Track::new();
let bottom = fixed(&mut t, 100.0, 100.0);
let top = fixed(&mut t, 100.0, 100.0);
t.add_root(Layer::Popup, None, bottom);
t.add_root(Layer::Popup, None, top);
relayout(&mut t);
assert_eq!(hit_path(&t, Point::new(50.0, 50.0)), vec![top]);
}
#[test]
fn path_to_returns_root_first() {
let (t, root, kids) = setup();
assert_eq!(path_to(&t, kids[0]), vec![root, kids[0]]);
assert!(path_to(&t, NodeId::NULL).is_empty());
}
#[test]
fn capture_redirects_the_path() {
let (mut t, root, kids) = setup();
let p = PointerId(0);
t.capture_pointer(p, kids[0]);
assert_eq!(
hit_path_for(&t, p, Point::new(250.0, 50.0)),
vec![root, kids[0]],
"捕获优先于真实位置"
);
t.release_pointer(p);
assert_eq!(hit_path_for(&t, p, Point::new(250.0, 50.0)).last(), Some(&kids[2]));
}
}