use std::collections::HashMap;
use glam::{Affine2, Vec2};
use martensite_core::{HotNode, NodeFlags, Rect, WidgetArena, WidgetId};
pub const SINGULAR_EPSILON: f32 = 1e-6;
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct HitTestResult {
pub widget_id: WidgetId,
pub local_point: Vec2,
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct AffineTransform {
inner: Affine2,
}
impl AffineTransform {
pub const IDENTITY: Self = Self {
inner: Affine2::IDENTITY,
};
#[inline]
#[must_use]
pub const fn from_affine(inner: Affine2) -> Self {
Self { inner }
}
#[inline]
#[must_use]
pub fn from_translation(translation: Vec2) -> Self {
Self {
inner: Affine2::from_translation(translation),
}
}
#[inline]
#[must_use]
pub fn from_scale(scale: Vec2) -> Self {
Self {
inner: Affine2::from_scale(scale),
}
}
#[inline]
#[must_use]
pub fn from_angle(angle: f32) -> Self {
Self {
inner: Affine2::from_angle(angle),
}
}
#[inline]
#[must_use]
pub fn from_scale_angle_translation(scale: Vec2, angle: f32, translation: Vec2) -> Self {
Self {
inner: Affine2::from_scale_angle_translation(scale, angle, translation),
}
}
#[inline]
#[must_use]
pub fn transform_point(&self, p: Vec2) -> Vec2 {
self.inner.transform_point2(p)
}
#[inline]
#[must_use]
pub fn determinant(&self) -> f32 {
self.inner.matrix2.determinant()
}
#[inline]
#[must_use]
pub fn is_singular(&self) -> bool {
let det = self.determinant();
!det.is_finite() || det.abs() < SINGULAR_EPSILON
}
#[inline]
#[must_use]
pub fn inverse(&self) -> Option<AffineTransform> {
if self.is_singular() {
None
} else {
Some(AffineTransform {
inner: self.inner.inverse(),
})
}
}
#[inline]
#[must_use]
pub const fn as_affine(&self) -> Affine2 {
self.inner
}
}
impl Default for AffineTransform {
#[inline]
fn default() -> Self {
Self::IDENTITY
}
}
impl From<Affine2> for AffineTransform {
#[inline]
fn from(inner: Affine2) -> Self {
Self::from_affine(inner)
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct RoundedRect {
pub rect: Rect,
pub radius: f32,
}
impl RoundedRect {
#[inline]
#[must_use]
pub fn new(rect: Rect, radius: f32) -> Self {
Self { rect, radius }
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum ClipShape {
Rect(Rect),
RoundedRect(RoundedRect),
Path(Vec<Vec2>),
}
#[must_use]
pub fn point_in_shape(shape: &ClipShape, point: Vec2) -> bool {
if !point.is_finite() {
return false;
}
match shape {
ClipShape::Rect(rect) => point_in_rect(point, *rect),
ClipShape::RoundedRect(rr) => point_in_rounded_rect(point, *rr),
ClipShape::Path(pts) => point_in_path(point, pts),
}
}
#[inline]
#[must_use]
pub fn point_in_rect(point: Vec2, rect: Rect) -> bool {
if !point.is_finite() {
return false;
}
point.x >= rect.min_x()
&& point.x < rect.max_x()
&& point.y >= rect.min_y()
&& point.y < rect.max_y()
}
fn point_in_rounded_rect(point: Vec2, rr: RoundedRect) -> bool {
let rect = rr.rect;
if !point_in_rect(point, rect) {
return false;
}
if rr.radius <= 0.0 {
return true;
}
let max_radius = rect.width().min(rect.height()) * 0.5;
let r = rr.radius.min(max_radius.max(0.0));
if r <= 0.0 {
return true;
}
let left = rect.min_x();
let right = rect.max_x();
let top = rect.min_y();
let bottom = rect.max_y();
let cx = if point.x < left + r {
left + r
} else if point.x > right - r {
right - r
} else {
return true; };
let cy = if point.y < top + r {
top + r
} else if point.y > bottom - r {
bottom - r
} else {
return true; };
let dx = point.x - cx;
let dy = point.y - cy;
dx * dx + dy * dy <= r * r
}
fn point_in_path(point: Vec2, pts: &[Vec2]) -> bool {
if pts.len() < 3 {
return false;
}
let n = pts.len();
let mut winding = 0i32;
let mut j = n - 1;
for i in 0..n {
let pi = pts[i];
let pj = pts[j];
if (pi.y <= point.y) != (pj.y <= point.y) {
let dy = pj.y - pi.y;
if dy.abs() > 0.0 {
let x_intersect = pi.x + ((point.y - pi.y) / dy) * (pj.x - pi.x);
if point.x < x_intersect {
if pi.y < pj.y {
winding += 1;
} else {
winding -= 1;
}
}
}
}
j = i;
}
winding != 0
}
pub struct HitTester<'a> {
arena: &'a WidgetArena,
}
impl<'a> HitTester<'a> {
#[inline]
#[must_use]
pub fn new(arena: &'a WidgetArena) -> Self {
Self { arena }
}
#[must_use]
pub fn hit_test(&self, root: WidgetId, screen_point: Vec2) -> Option<HitTestResult> {
if !screen_point.is_finite() || !self.arena.is_alive(root) {
return None;
}
self.hit_test_node(root, screen_point, None, None)
}
#[must_use]
pub fn hit_test_with_transforms(
&self,
root: WidgetId,
screen_point: Vec2,
transforms: &HashMap<WidgetId, AffineTransform>,
) -> Option<HitTestResult> {
if !screen_point.is_finite() || !self.arena.is_alive(root) {
return None;
}
self.hit_test_node(root, screen_point, Some(transforms), None)
}
#[must_use]
pub fn hit_test_with_clip(
&self,
root: WidgetId,
screen_point: Vec2,
clips: &HashMap<WidgetId, ClipShape>,
) -> Option<HitTestResult> {
if !screen_point.is_finite() || !self.arena.is_alive(root) {
return None;
}
self.hit_test_node(root, screen_point, None, Some(clips))
}
#[must_use]
pub fn hit_test_full(
&self,
root: WidgetId,
screen_point: Vec2,
transforms: &HashMap<WidgetId, AffineTransform>,
clips: &HashMap<WidgetId, ClipShape>,
) -> Option<HitTestResult> {
if !screen_point.is_finite() || !self.arena.is_alive(root) {
return None;
}
self.hit_test_node(root, screen_point, Some(transforms), Some(clips))
}
#[allow(clippy::too_many_lines)]
fn hit_test_node(
&self,
node: WidgetId,
screen_point: Vec2,
transforms: Option<&HashMap<WidgetId, AffineTransform>>,
clips: Option<&HashMap<WidgetId, ClipShape>>,
) -> Option<HitTestResult> {
let hot: &HotNode = self.arena.get_hot(node)?;
if !hot.flags.contains(NodeFlags::VISIBLE) {
return None;
}
let mut child = self.arena.last_child(node);
while let Some(child_id) = child {
if let Some(hit) = self.hit_test_node(child_id, screen_point, transforms, clips) {
return Some(hit);
}
child = self.arena.prev_sibling(child_id);
}
let flags = hot.flags;
if !flags.contains(NodeFlags::HIT_TEST_ENABLED) || flags.contains(NodeFlags::INERT) {
return None;
}
if !point_in_rect(screen_point, hot.bounds) {
return None;
}
let local_point = if let Some(map) = transforms {
if let Some(transform) = map.get(&node) {
let inverse = transform.inverse()?;
let local = inverse.transform_point(screen_point);
if !local.is_finite() {
return None;
}
let local_rect = Rect::new(0.0, 0.0, hot.bounds.width(), hot.bounds.height());
if !point_in_rect(local, local_rect) {
return None;
}
local
} else {
screen_point
}
} else {
screen_point
};
if let Some(map) = clips {
if let Some(shape) = map.get(&node) {
if !point_in_shape(shape, local_point) {
return None;
}
}
}
Some(HitTestResult {
widget_id: node,
local_point,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use martensite_core::{ColdNode, HotNode, NodeFlags, Rect, WidgetArena};
use std::f32::consts::{FRAC_PI_2, FRAC_PI_4};
use std::panic::AssertUnwindSafe;
fn hot_node(x: f32, y: f32, w: f32, h: f32) -> HotNode {
HotNode {
bounds: Rect::new(x, y, w, h),
flags: NodeFlags::VISIBLE | NodeFlags::HIT_TEST_ENABLED,
..HotNode::default()
}
}
fn insert(arena: &mut WidgetArena, x: f32, y: f32, w: f32, h: f32) -> WidgetId {
arena.insert(hot_node(x, y, w, h), ColdNode::default())
}
#[test]
fn basic_aabb_hit_inside() {
let mut arena = WidgetArena::new();
let root = insert(&mut arena, 0.0, 0.0, 100.0, 100.0);
let tester = HitTester::new(&arena);
let hit = tester.hit_test(root, Vec2::new(50.0, 50.0));
assert_eq!(hit.map(|h| h.widget_id), Some(root));
assert_eq!(hit.unwrap().local_point, Vec2::new(50.0, 50.0));
}
#[test]
fn basic_aabb_hit_outside() {
let mut arena = WidgetArena::new();
let root = insert(&mut arena, 0.0, 0.0, 100.0, 100.0);
let tester = HitTester::new(&arena);
assert!(tester.hit_test(root, Vec2::new(150.0, 150.0)).is_none());
}
#[test]
fn nested_child_wins_over_parent() {
let mut arena = WidgetArena::new();
let parent = insert(&mut arena, 0.0, 0.0, 200.0, 200.0);
let child = insert(&mut arena, 50.0, 50.0, 100.0, 100.0);
arena.append_child(parent, child).unwrap();
let tester = HitTester::new(&arena);
let hit = tester.hit_test(parent, Vec2::new(75.0, 75.0));
assert_eq!(hit.map(|h| h.widget_id), Some(child));
let hit = tester.hit_test(parent, Vec2::new(10.0, 10.0));
assert_eq!(hit.map(|h| h.widget_id), Some(parent));
}
#[test]
fn reverse_z_order_topmost_sibling_wins() {
let mut arena = WidgetArena::new();
let root = insert(&mut arena, 0.0, 0.0, 200.0, 200.0);
let bottom = insert(&mut arena, 0.0, 0.0, 100.0, 100.0);
let top = insert(&mut arena, 0.0, 0.0, 100.0, 100.0);
arena.append_child(root, bottom).unwrap();
arena.append_child(root, top).unwrap();
let tester = HitTester::new(&arena);
let hit = tester.hit_test(root, Vec2::new(50.0, 50.0));
assert_eq!(hit.map(|h| h.widget_id), Some(top));
}
#[test]
fn transformed_translation() {
let mut arena = WidgetArena::new();
let root = insert(&mut arena, 100.0, 100.0, 100.0, 100.0);
let mut transforms = HashMap::new();
transforms.insert(
root,
AffineTransform::from_translation(Vec2::new(100.0, 100.0)),
);
let tester = HitTester::new(&arena);
let hit = tester.hit_test_with_transforms(root, Vec2::new(150.0, 150.0), &transforms);
assert_eq!(hit.map(|h| h.widget_id), Some(root));
assert_eq!(hit.unwrap().local_point, Vec2::new(50.0, 50.0));
assert!(tester
.hit_test_with_transforms(root, Vec2::new(50.0, 50.0), &transforms)
.is_none());
}
#[test]
fn transformed_rotation_90_degrees() {
let mut arena = WidgetArena::new();
let root = insert(&mut arena, -100.0, 0.0, 100.0, 100.0);
let mut transforms = HashMap::new();
transforms.insert(root, AffineTransform::from_angle(FRAC_PI_2));
let tester = HitTester::new(&arena);
let hit = tester.hit_test_with_transforms(root, Vec2::new(-50.0, 50.0), &transforms);
assert_eq!(hit.map(|h| h.widget_id), Some(root));
let local = hit.unwrap().local_point;
assert!((local.x - 50.0).abs() < 1e-3);
assert!((local.y - 50.0).abs() < 1e-3);
}
#[test]
fn transformed_scale_2x() {
let mut arena = WidgetArena::new();
let root = insert(&mut arena, 0.0, 0.0, 200.0, 200.0);
let mut transforms = HashMap::new();
transforms.insert(root, AffineTransform::from_scale(Vec2::new(2.0, 2.0)));
let tester = HitTester::new(&arena);
let hit = tester.hit_test_with_transforms(root, Vec2::new(150.0, 150.0), &transforms);
assert_eq!(hit.map(|h| h.widget_id), Some(root));
assert_eq!(hit.unwrap().local_point, Vec2::new(75.0, 75.0));
}
#[test]
fn singular_zero_scale_returns_no_hit_without_panic() {
let mut arena = WidgetArena::new();
let root = insert(&mut arena, 0.0, 0.0, 100.0, 100.0);
let mut transforms = HashMap::new();
transforms.insert(root, AffineTransform::from_scale(Vec2::new(0.0, 0.0)));
let tester = HitTester::new(&arena);
assert!(transforms.get(&root).unwrap().is_singular());
let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
tester.hit_test_with_transforms(root, Vec2::new(50.0, 50.0), &transforms)
}));
assert!(result.is_ok());
assert!(result.unwrap().is_none());
}
#[test]
fn singular_edge_on_scale_returns_no_hit() {
let mut arena = WidgetArena::new();
let root = insert(&mut arena, 0.0, 0.0, 100.0, 100.0);
let mut transforms = HashMap::new();
transforms.insert(root, AffineTransform::from_scale(Vec2::new(1e-8, 1.0)));
let tester = HitTester::new(&arena);
assert!(transforms.get(&root).unwrap().is_singular());
assert!(tester
.hit_test_with_transforms(root, Vec2::new(50.0, 50.0), &transforms)
.is_none());
}
#[test]
fn nan_input_does_not_panic() {
let mut arena = WidgetArena::new();
let root = insert(&mut arena, 0.0, 0.0, 100.0, 100.0);
let tester = HitTester::new(&arena);
let nan = Vec2::new(f32::NAN, 50.0);
let result = std::panic::catch_unwind(AssertUnwindSafe(|| tester.hit_test(root, nan)));
assert!(result.is_ok());
assert!(result.unwrap().is_none());
let inf = Vec2::new(f32::INFINITY, 50.0);
assert!(tester.hit_test(root, inf).is_none());
}
#[test]
fn nan_inf_affine_matrix_components_are_singular() {
use glam::Affine2;
let nan_matrix = Affine2::from_cols_array(&[f32::NAN, 0.0, 0.0, 1.0, 0.0, 0.0]);
let nan_transform = AffineTransform::from_affine(nan_matrix);
assert!(nan_transform.is_singular());
assert!(nan_transform.inverse().is_none());
let inf_matrix = Affine2::from_cols_array(&[f32::INFINITY, 0.0, 0.0, 1.0, 0.0, 0.0]);
let inf_transform = AffineTransform::from_affine(inf_matrix);
assert!(inf_transform.is_singular());
assert!(inf_transform.inverse().is_none());
let mut arena = WidgetArena::new();
let root = insert(&mut arena, 0.0, 0.0, 100.0, 100.0);
let mut transforms = HashMap::new();
transforms.insert(root, nan_transform);
let tester = HitTester::new(&arena);
let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
tester.hit_test_with_transforms(root, Vec2::new(50.0, 50.0), &transforms)
}));
assert!(result.is_ok());
assert!(result.unwrap().is_none());
}
#[test]
fn empty_tree_no_children() {
let mut arena = WidgetArena::new();
let root = insert(&mut arena, 0.0, 0.0, 100.0, 100.0);
let tester = HitTester::new(&arena);
assert_eq!(
tester
.hit_test(root, Vec2::new(50.0, 50.0))
.map(|h| h.widget_id),
Some(root)
);
assert!(tester.hit_test(root, Vec2::new(150.0, 150.0)).is_none());
}
#[test]
fn boundary_min_inclusive_max_exclusive() {
let mut arena = WidgetArena::new();
let root = insert(&mut arena, 10.0, 10.0, 100.0, 100.0);
let tester = HitTester::new(&arena);
assert!(tester.hit_test(root, Vec2::new(10.0, 10.0)).is_some());
assert!(tester.hit_test(root, Vec2::new(110.0, 10.0)).is_none());
assert!(tester.hit_test(root, Vec2::new(10.0, 110.0)).is_none());
}
#[test]
fn deeply_nested_tree_10_levels() {
let mut arena = WidgetArena::new();
let mut current = insert(&mut arena, 0.0, 0.0, 200.0, 200.0);
for _ in 0..10 {
let child = insert(&mut arena, 10.0, 10.0, 180.0, 180.0);
arena.append_child(current, child).unwrap();
current = child;
}
let tester = HitTester::new(&arena);
let root = arena.iter_breadth_first().next().expect("root exists");
let hit = tester.hit_test(root, Vec2::new(15.0, 15.0));
assert_eq!(hit.map(|h| h.widget_id), Some(current));
}
#[test]
fn rounded_rect_corner_miss() {
let mut arena = WidgetArena::new();
let root = insert(&mut arena, 0.0, 0.0, 100.0, 100.0);
let mut clips = HashMap::new();
clips.insert(
root,
ClipShape::RoundedRect(RoundedRect::new(Rect::new(0.0, 0.0, 100.0, 100.0), 20.0)),
);
let tester = HitTester::new(&arena);
assert!(tester
.hit_test_with_clip(root, Vec2::new(50.0, 50.0), &clips)
.is_some());
assert!(tester
.hit_test_with_clip(root, Vec2::new(5.0, 5.0), &clips)
.is_none());
}
#[test]
fn path_clip_star_shape_winding() {
let mut arena = WidgetArena::new();
let root = insert(&mut arena, -50.0, -50.0, 100.0, 100.0);
let diamond = vec![
Vec2::new(0.0, -40.0),
Vec2::new(40.0, 0.0),
Vec2::new(0.0, 40.0),
Vec2::new(-40.0, 0.0),
];
let mut clips = HashMap::new();
clips.insert(root, ClipShape::Path(diamond));
let tester = HitTester::new(&arena);
assert!(tester
.hit_test_with_clip(root, Vec2::new(0.0, 0.0), &clips)
.is_some());
assert!(tester
.hit_test_with_clip(root, Vec2::new(-45.0, -45.0), &clips)
.is_none());
}
#[test]
fn path_clip_self_intersecting_winding_number() {
let mut arena = WidgetArena::new();
let root = insert(&mut arena, 0.0, 0.0, 120.0, 120.0);
let double_square = vec![
Vec2::new(0.0, 0.0),
Vec2::new(60.0, 0.0),
Vec2::new(60.0, 60.0),
Vec2::new(0.0, 60.0),
Vec2::new(0.0, 0.0),
Vec2::new(40.0, 40.0),
Vec2::new(100.0, 40.0),
Vec2::new(100.0, 100.0),
Vec2::new(40.0, 100.0),
Vec2::new(40.0, 40.0),
];
let mut clips = HashMap::new();
clips.insert(root, ClipShape::Path(double_square));
let tester = HitTester::new(&arena);
assert!(tester
.hit_test_with_clip(root, Vec2::new(20.0, 30.0), &clips)
.is_some());
assert!(tester
.hit_test_with_clip(root, Vec2::new(80.0, 70.0), &clips)
.is_some());
assert!(
tester
.hit_test_with_clip(root, Vec2::new(50.0, 50.0), &clips)
.is_some(),
"overlap region must be inside under winding-number rule"
);
assert!(tester
.hit_test_with_clip(root, Vec2::new(110.0, 10.0), &clips)
.is_none());
}
#[test]
fn invisible_widget_skipped() {
let mut arena = WidgetArena::new();
let root = insert(&mut arena, 0.0, 0.0, 100.0, 100.0);
if let Some(hot) = arena.get_hot_mut(root) {
hot.flags.remove(NodeFlags::VISIBLE);
}
let tester = HitTester::new(&arena);
assert!(tester.hit_test(root, Vec2::new(50.0, 50.0)).is_none());
}
#[test]
fn inert_widget_skipped() {
let mut arena = WidgetArena::new();
let root = insert(&mut arena, 0.0, 0.0, 100.0, 100.0);
if let Some(hot) = arena.get_hot_mut(root) {
hot.flags |= NodeFlags::INERT;
}
let tester = HitTester::new(&arena);
assert!(tester.hit_test(root, Vec2::new(50.0, 50.0)).is_none());
}
#[test]
fn hit_test_enabled_flag_gated() {
let mut arena = WidgetArena::new();
let root = insert(&mut arena, 0.0, 0.0, 100.0, 100.0);
if let Some(hot) = arena.get_hot_mut(root) {
hot.flags.remove(NodeFlags::HIT_TEST_ENABLED);
}
let tester = HitTester::new(&arena);
assert!(tester.hit_test(root, Vec2::new(50.0, 50.0)).is_none());
}
#[test]
fn dead_root_returns_none() {
let mut arena = WidgetArena::new();
let root = insert(&mut arena, 0.0, 0.0, 100.0, 100.0);
arena.remove(root);
let tester = HitTester::new(&arena);
assert!(tester.hit_test(root, Vec2::new(50.0, 50.0)).is_none());
}
#[test]
fn stress_test_100k_synthetic_clicks() {
let mut arena = WidgetArena::new();
let root = insert(&mut arena, 0.0, 0.0, 400.0, 400.0);
let collapsed = arena.insert(
HotNode {
bounds: Rect::new(200.0, 200.0, 0.0, 0.0),
flags: NodeFlags::VISIBLE | NodeFlags::HIT_TEST_ENABLED,
..HotNode::default()
},
ColdNode::default(),
);
arena.append_child(root, collapsed).unwrap();
let scaled = insert(&mut arena, 0.0, 0.0, 200.0, 200.0);
arena.append_child(root, scaled).unwrap();
let rotated = insert(&mut arena, -200.0, 0.0, 200.0, 200.0);
arena.append_child(root, rotated).unwrap();
let mut transforms = HashMap::new();
transforms.insert(scaled, AffineTransform::from_scale(Vec2::new(2.0, 0.5)));
transforms.insert(rotated, AffineTransform::from_angle(FRAC_PI_4));
transforms.insert(collapsed, AffineTransform::from_scale(Vec2::new(0.0, 0.0)));
let tester = HitTester::new(&arena);
let mut state: u64 = 0x1234_5678_9abc_def0;
let mut next = || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state
};
let to_f32 = |v: u64| -> f32 {
let bits = (v & 0xFFFF_FFFF) as u32;
match bits % 64 {
0 => f32::NAN,
1 => f32::INFINITY,
2 => f32::NEG_INFINITY,
_ => (bits % 2000) as f32 - 1000.0,
}
};
for _ in 0..100_000 {
let x = to_f32(next());
let y = to_f32(next());
let point = Vec2::new(x, y);
let _ = tester.hit_test(root, point);
let _ = tester.hit_test_with_transforms(root, point, &transforms);
let _ = tester.hit_test_full(root, point, &transforms, &HashMap::new());
}
}
#[test]
fn affine_transform_identity_round_trip() {
let t = AffineTransform::IDENTITY;
assert_eq!(t.transform_point(Vec2::new(5.0, 7.0)), Vec2::new(5.0, 7.0));
let inv = t.inverse().unwrap();
assert_eq!(
inv.transform_point(Vec2::new(5.0, 7.0)),
Vec2::new(5.0, 7.0)
);
assert!(!t.is_singular());
}
#[test]
fn point_in_rect_non_finite() {
let r = Rect::new(0.0, 0.0, 10.0, 10.0);
assert!(!point_in_rect(Vec2::new(f32::NAN, 5.0), r));
assert!(!point_in_rect(Vec2::new(5.0, f32::INFINITY), r));
}
#[test]
fn point_in_path_degenerate() {
assert!(!point_in_path(Vec2::new(1.0, 1.0), &[]));
assert!(!point_in_path(Vec2::new(1.0, 1.0), &[Vec2::new(0.0, 0.0)]));
assert!(!point_in_path(
Vec2::new(1.0, 1.0),
&[Vec2::new(0.0, 0.0), Vec2::new(2.0, 2.0)]
));
}
#[test]
fn clip_shape_rect_evaluates() {
let mut arena = WidgetArena::new();
let root = insert(&mut arena, 0.0, 0.0, 100.0, 100.0);
let mut clips = HashMap::new();
clips.insert(root, ClipShape::Rect(Rect::new(25.0, 25.0, 50.0, 50.0)));
let tester = HitTester::new(&arena);
assert!(tester
.hit_test_with_clip(root, Vec2::new(40.0, 40.0), &clips)
.is_some());
assert!(tester
.hit_test_with_clip(root, Vec2::new(10.0, 10.0), &clips)
.is_none());
}
}