use std::cell::Cell;
use super::*;
use crate::pointer_slices::pointer_slices;
fn rect_to_quad(rect: Rect) -> [[f32; 2]; 4] {
[
[rect.x, rect.y],
[rect.x + rect.width, rect.y],
[rect.x, rect.y + rect.height],
[rect.x + rect.width, rect.y + rect.height],
]
}
fn translated_world_to_local(rect: Rect) -> ProjectiveTransform {
ProjectiveTransform::translation(-rect.x, -rect.y)
}
fn local_bounds_for_rect(rect: Rect) -> Rect {
Rect {
x: 0.0,
y: 0.0,
width: rect.width,
height: rect.height,
}
}
fn hit_geometry_for_rect(rect: Rect) -> HitGeometry<'static> {
HitGeometry {
rect,
quad: rect_to_quad(rect),
local_bounds: local_bounds_for_rect(rect),
world_to_local: translated_world_to_local(rect),
hit_clip_bounds: None,
hit_clips: &[],
}
}
fn test_diagnostics() -> Rc<RenderDiagnostics> {
Rc::new(RenderDiagnostics::new())
}
fn make_handler(counter: Rc<Cell<u32>>, consume: bool) -> Rc<dyn Fn(PointerEvent)> {
Rc::new(move |event: PointerEvent| {
counter.set(counter.get() + 1);
if consume {
event.consume();
}
})
}
#[test]
fn rebuilding_hits_reuses_buffers_releases_handlers_and_preserves_captured_targets() {
let mut scene = Scene::new();
let first_count = Rc::new(Cell::new(0));
let second_count = Rc::new(Cell::new(0));
let first = pointer_slices(&[make_handler(Rc::clone(&first_count), false)]);
let second = pointer_slices(&[make_handler(Rc::clone(&second_count), false)]);
let rect = Rect {
x: 0.0,
y: 0.0,
width: 30.0,
height: 30.0,
};
scene.push_hit(
1,
&[1, 9],
hit_geometry_for_rect(rect),
HitTargetSpec {
shape: None,
handlers: &first,
},
);
let captured = scene.find_target(1).expect("the first target");
let path_storage = scene.hits[0].capture_path.as_ptr();
scene.clear_hits();
assert!(scene.hits.is_empty());
assert!(scene.find_target(1).is_none());
assert_eq!(scene.next_hit_z, 0);
assert_eq!(
Rc::strong_count(&first),
2,
"after a clear only the captured target still shares the first slices"
);
scene.push_hit(
2,
&[2],
hit_geometry_for_rect(rect),
HitTargetSpec {
shape: None,
handlers: &second,
},
);
assert_eq!(scene.hits[0].capture_path.as_ptr(), path_storage);
assert_eq!(scene.hits[0].capture_path, [2]);
assert_eq!(scene.hits[0].z_index, 0);
assert!(Rc::ptr_eq(&scene.hits[0].handlers, &second));
let event = PointerEvent::new(
PointerEventKind::Down,
Point::new(5.0, 5.0),
Point::new(5.0, 5.0),
);
scene
.find_target(2)
.expect("the second target")
.dispatch(event.clone());
assert_eq!(first_count.get(), 0);
assert_eq!(second_count.get(), 1);
captured.dispatch(event);
assert_eq!(captured.capture_path, [1, 9]);
assert_eq!(first_count.get(), 1);
scene.clear_hits();
assert_eq!(Rc::strong_count(&second), 1);
scene.push_hit(
3,
&[3],
hit_geometry_for_rect(rect),
HitTargetSpec {
shape: None,
handlers: &pointer_slices(&[]),
},
);
assert!(scene.hits.is_empty());
assert_eq!(scene.next_hit_z, 0);
}
#[test]
fn collecting_observation_owners_clears_an_empty_scene() {
let scene = Scene::new();
let mut nodes = HashSet::from_iter([13, 17]);
let capacity = nodes.capacity();
assert!(scene.collect_retained_visual_observation_nodes(&mut nodes));
assert!(nodes.is_empty());
assert_eq!(nodes.capacity(), capacity);
}
#[test]
fn rebuilding_clip_buffers_replaces_clips_without_changing_captured_targets() {
let mut scene = Scene::new();
let rect = Rect {
x: 0.0,
y: 0.0,
width: 100.0,
height: 100.0,
};
let left = Rect {
width: 50.0,
..rect
};
let right = Rect {
x: 50.0,
width: 50.0,
..rect
};
let clip = |bounds| HitClip {
quad: rect_to_quad(bounds),
bounds,
};
let handler = make_handler(Rc::new(Cell::new(0)), false);
let push = |scene: &mut Scene, clips: &[HitClip]| {
scene.push_hit(
1,
&[1],
HitGeometry {
hit_clips: clips,
..hit_geometry_for_rect(rect)
},
HitTargetSpec {
shape: None,
handlers: &pointer_slices(&[Rc::clone(&handler)]),
},
);
};
push(&mut scene, &[clip(left)]);
let captured = scene.find_target(1).unwrap();
let storage = scene.hits[0].hit_clips.as_ptr();
assert!(captured.contains(25.0, 25.0));
assert!(!captured.contains(75.0, 25.0));
scene.clear_hits();
push(&mut scene, &[clip(right)]);
assert_eq!(scene.hits[0].hit_clips.as_ptr(), storage);
assert!(scene.hit_test(25.0, 25.0).is_empty());
assert_eq!(scene.hit_test(75.0, 25.0).len(), 1);
assert!(captured.contains(25.0, 25.0));
assert!(!captured.contains(75.0, 25.0));
scene.clear_hits();
push(&mut scene, &[]);
assert_eq!(scene.hit_test(25.0, 25.0).len(), 1);
assert_eq!(scene.hit_test(75.0, 25.0).len(), 1);
}
#[test]
fn hit_test_respects_hit_clip() {
let mut scene = Scene::new();
let rect = Rect {
x: 0.0,
y: 0.0,
width: 100.0,
height: 100.0,
};
let clip = Rect {
x: 0.0,
y: 0.0,
width: 40.0,
height: 40.0,
};
scene.push_hit(
1,
&[1],
HitGeometry {
hit_clip_bounds: Some(clip),
hit_clips: &[HitClip {
quad: rect_to_quad(clip),
bounds: clip,
}],
..hit_geometry_for_rect(rect)
},
HitTargetSpec {
shape: None,
handlers: &pointer_slices(&[Rc::new(|_event: PointerEvent| {})]),
},
);
assert!(scene.hit_test(60.0, 20.0).is_empty());
assert_eq!(scene.hit_test(20.0, 20.0).len(), 1);
}
fn push_input_target(scene: &mut Scene, node_id: NodeId, rect: Rect) {
scene.push_hit(
node_id,
&[node_id],
hit_geometry_for_rect(rect),
HitTargetSpec {
shape: None,
handlers: &pointer_slices(&[Rc::new(|_event: PointerEvent| {})]),
},
);
}
fn small_target(x: f32) -> Rect {
Rect {
x,
y: 100.0,
width: 20.0,
height: 20.0,
}
}
#[test]
fn a_press_beside_a_small_target_reaches_it_when_nothing_else_claims_the_point() {
let mut scene = Scene::new();
push_input_target(&mut scene, 1, small_target(100.0));
assert!(scene.hit_test(125.0, 110.0).is_empty());
let near = scene
.hit_test_near(125.0, 110.0)
.expect("the press lands inside the target grown to 48");
assert_eq!(near.node_id, 1);
assert!(scene.hit_test_near(140.0, 110.0).is_none());
}
#[test]
fn the_nearest_small_target_takes_the_press() {
let mut scene = Scene::new();
push_input_target(&mut scene, 1, small_target(100.0));
push_input_target(&mut scene, 2, small_target(140.0));
assert_eq!(
scene.hit_test_near(122.0, 110.0).map(|hit| hit.node_id),
Some(1)
);
assert_eq!(
scene.hit_test_near(138.0, 110.0).map(|hit| hit.node_id),
Some(2)
);
}
#[test]
fn a_target_of_the_minimum_size_has_no_reach_beyond_its_box() {
let mut scene = Scene::new();
push_input_target(
&mut scene,
1,
Rect {
x: 0.0,
y: 0.0,
width: 48.0,
height: 48.0,
},
);
assert!(scene.hit_test_near(50.0, 10.0).is_none());
}
#[test]
fn hit_test_sorts_by_z_without_duplicating_hit_storage() {
let mut scene = Scene::new();
let rect = Rect {
x: 0.0,
y: 0.0,
width: 50.0,
height: 50.0,
};
scene.push_hit(
1,
&[1],
hit_geometry_for_rect(rect),
HitTargetSpec {
shape: None,
handlers: &pointer_slices(&[Rc::new(|_event: PointerEvent| {})]),
},
);
scene.push_hit(
2,
&[2],
hit_geometry_for_rect(rect),
HitTargetSpec {
shape: None,
handlers: &pointer_slices(&[Rc::new(|_event: PointerEvent| {})]),
},
);
assert_eq!(scene.node_index.get(&1), Some(&0));
assert_eq!(scene.node_index.get(&2), Some(&1));
let hits = scene.hit_test(10.0, 10.0);
assert_eq!(
hits.iter().map(|hit| hit.node_id).collect::<Vec<_>>(),
vec![2, 1]
);
assert_eq!(scene.find_target(1).map(|hit| hit.node_id), Some(1));
assert_eq!(scene.find_target(2).map(|hit| hit.node_id), Some(2));
}
#[test]
fn hit_test_rejects_points_in_rounded_corner_cutout() {
let mut scene = Scene::new();
let rect = Rect {
x: 0.0,
y: 0.0,
width: 40.0,
height: 40.0,
};
scene.push_hit(
1,
&[1],
hit_geometry_for_rect(rect),
HitTargetSpec {
shape: Some(RoundedCornerShape::uniform(20.0)),
handlers: &pointer_slices(&[Rc::new(|_event: PointerEvent| {})]),
},
);
assert!(scene.hit_test(1.0, 1.0).is_empty());
assert_eq!(scene.hit_test(20.0, 20.0).len(), 1);
}
#[test]
fn render_diagnostics_claim_each_warning_key_once() {
let diagnostics = RenderDiagnostics::new();
assert!(diagnostics.claim_warning_once("pixels.effect-fallback"));
assert!(!diagnostics.claim_warning_once("pixels.effect-fallback"));
assert!(diagnostics.claim_warning_once("pixels.blend-fallback"));
}
#[test]
fn dispatch_stops_after_event_consumed() {
let count_first = Rc::new(Cell::new(0));
let count_second = Rc::new(Cell::new(0));
let hit = HitRegion::with_diagnostics(HitRegionInit {
node_id: 1,
capture_path: vec![1],
geometry: hit_geometry_for_rect(Rect {
x: 0.0,
y: 0.0,
width: 50.0,
height: 50.0,
}),
handlers: pointer_slices(&[
make_handler(count_first.clone(), true),
make_handler(count_second.clone(), false),
]),
..Default::default()
});
let event = PointerEvent::new(
PointerEventKind::Down,
Point { x: 10.0, y: 10.0 },
Point { x: 10.0, y: 10.0 },
);
hit.dispatch(event);
assert_eq!(count_first.get(), 1);
assert_eq!(count_second.get(), 0);
}
#[test]
fn dispatch_delivers_terminal_events_after_consumption_for_cleanup() {
let count_first = Rc::new(Cell::new(0));
let count_second = Rc::new(Cell::new(0));
let hit = HitRegion::with_diagnostics(HitRegionInit {
node_id: 1,
capture_path: vec![1],
geometry: hit_geometry_for_rect(Rect {
x: 0.0,
y: 0.0,
width: 50.0,
height: 50.0,
}),
handlers: pointer_slices(&[
make_handler(count_first.clone(), true),
make_handler(count_second.clone(), false),
]),
..Default::default()
});
for kind in [PointerEventKind::Up, PointerEventKind::Cancel] {
let event = PointerEvent::new(kind, Point { x: 10.0, y: 10.0 }, Point { x: 10.0, y: 10.0 });
hit.dispatch(event);
}
assert_eq!(count_first.get(), 2);
assert_eq!(count_second.get(), 2);
}
#[test]
fn dispatch_delivers_terminal_events_to_later_captured_targets_after_consumption() {
let child_count = Rc::new(Cell::new(0));
let parent_count = Rc::new(Cell::new(0));
let child_hit = HitRegion::with_diagnostics(HitRegionInit {
node_id: 2,
capture_path: vec![2, 1],
geometry: hit_geometry_for_rect(Rect {
x: 8.0,
y: 8.0,
width: 20.0,
height: 20.0,
}),
handlers: pointer_slices(&[make_handler(child_count.clone(), true)]),
z_index: 1,
..Default::default()
});
let parent_hit = HitRegion::with_diagnostics(HitRegionInit {
node_id: 1,
capture_path: vec![1],
geometry: hit_geometry_for_rect(Rect {
x: 0.0,
y: 0.0,
width: 50.0,
height: 50.0,
}),
handlers: pointer_slices(&[make_handler(parent_count.clone(), false)]),
..Default::default()
});
let event = PointerEvent::new(
PointerEventKind::Up,
Point { x: 12.0, y: 12.0 },
Point { x: 12.0, y: 12.0 },
);
child_hit.dispatch(event.clone());
parent_hit.dispatch(event);
assert_eq!(child_count.get(), 1);
assert_eq!(parent_count.get(), 1);
}
#[test]
fn dispatch_passes_local_position_to_pointer_inputs() {
let local_positions = Rc::new(RefCell::new(Vec::new()));
let local_positions_for_handler = Rc::clone(&local_positions);
let handler: Rc<dyn Fn(PointerEvent)> = Rc::new(move |event: PointerEvent| {
local_positions_for_handler
.borrow_mut()
.push(event.position);
});
let hit = HitRegion::with_diagnostics(HitRegionInit {
node_id: 1,
capture_path: vec![1],
geometry: hit_geometry_for_rect(Rect {
x: 10.0,
y: 12.0,
width: 50.0,
height: 50.0,
}),
handlers: pointer_slices(&[handler]),
..Default::default()
});
hit.dispatch(PointerEvent::new(
PointerEventKind::Down,
Point { x: 15.0, y: 17.0 },
Point { x: 15.0, y: 17.0 },
));
assert_eq!(*local_positions.borrow(), vec![Point { x: 5.0, y: 5.0 }]);
}
#[test]
fn hit_test_uses_exact_quad_for_transformed_region() {
let mut scene = Scene::new();
let rect = Rect {
x: 0.0,
y: 0.0,
width: 40.0,
height: 20.0,
};
let quad = [[10.0, 10.0], [50.0, 10.0], [20.0, 30.0], [60.0, 30.0]];
let world_to_local = ProjectiveTransform::from_rect_to_quad(rect, quad)
.inverse()
.expect("transformed hit region should be invertible");
scene.push_hit(
1,
&[1],
HitGeometry {
rect: Rect {
x: 10.0,
y: 10.0,
width: 50.0,
height: 20.0,
},
quad,
local_bounds: rect,
world_to_local,
hit_clip_bounds: None,
hit_clips: &[],
},
HitTargetSpec {
shape: None,
handlers: &pointer_slices(&[Rc::new(|_event: PointerEvent| {})]),
},
);
assert!(
scene.hit_test(15.0, 28.0).is_empty(),
"point inside the quad bounds but outside the transformed quad must not hit"
);
assert_eq!(scene.hit_test(30.0, 20.0).len(), 1);
}
#[test]
fn dispatch_uses_inverse_transform_for_local_position() {
let local_positions = Rc::new(RefCell::new(Vec::new()));
let local_positions_for_handler = Rc::clone(&local_positions);
let handler: Rc<dyn Fn(PointerEvent)> = Rc::new(move |event: PointerEvent| {
local_positions_for_handler
.borrow_mut()
.push(event.position);
});
let local_bounds = Rect {
x: 0.0,
y: 0.0,
width: 20.0,
height: 10.0,
};
let quad = [[20.0, 10.0], [60.0, 10.0], [20.0, 30.0], [60.0, 30.0]];
let world_to_local = ProjectiveTransform::from_rect_to_quad(local_bounds, quad)
.inverse()
.expect("translated quad should be invertible");
let hit = HitRegion::with_diagnostics(HitRegionInit {
node_id: 1,
capture_path: vec![1],
geometry: HitGeometry {
rect: Rect {
x: 20.0,
y: 10.0,
width: 40.0,
height: 20.0,
},
quad,
local_bounds,
world_to_local,
hit_clip_bounds: None,
hit_clips: &[],
},
handlers: pointer_slices(&[handler]),
..Default::default()
});
hit.dispatch(PointerEvent::new(
PointerEventKind::Down,
Point { x: 25.0, y: 17.0 },
Point { x: 25.0, y: 17.0 },
));
assert_eq!(*local_positions.borrow(), vec![Point { x: 2.5, y: 3.5 }]);
}
#[test]
fn dispatch_with_applier_counts_live_modifier_slice_lookup_misses() {
let handler_calls = Rc::new(Cell::new(0));
let handler_calls_for_handler = Rc::clone(&handler_calls);
let diagnostics = test_diagnostics();
let hit = HitRegion::with_diagnostics(HitRegionInit {
node_id: 42,
capture_path: vec![42],
geometry: hit_geometry_for_rect(Rect {
x: 0.0,
y: 0.0,
width: 50.0,
height: 50.0,
}),
handlers: pointer_slices(&[Rc::new(move |_event: PointerEvent| {
handler_calls_for_handler.set(handler_calls_for_handler.get() + 1);
})]),
diagnostics: Rc::clone(&diagnostics),
..Default::default()
});
let misses_before = diagnostics.live_modifier_slice_lookup_miss_count();
let mut applier = MemoryApplier::new();
hit.dispatch_with_applier(
&mut applier,
PointerEvent::new(
PointerEventKind::Down,
Point { x: 10.0, y: 10.0 },
Point { x: 10.0, y: 10.0 },
),
);
assert_eq!(handler_calls.get(), 1);
assert_eq!(
diagnostics.live_modifier_slice_lookup_miss_count(),
misses_before + 1
);
}