1use std::cell::RefCell;
2use std::cmp::Reverse;
3use std::collections::HashMap;
4use std::rc::Rc;
5use std::sync::atomic::{AtomicUsize, Ordering};
6
7use cranpose_core::{MemoryApplier, NodeId};
8use cranpose_foundation::{PointerEvent, PointerEventKind};
9use cranpose_ui::{LayoutNode, ModifierNodeSlices, SubcomposeLayoutNode};
10use cranpose_ui_graphics::{Point, Rect, RoundedCornerShape};
11
12use crate::graph::{ProjectiveTransform, RenderGraph};
13use crate::{HitTestTarget, RenderScene};
14
15static LIVE_MODIFIER_SLICE_LOOKUP_MISS_COUNT: AtomicUsize = AtomicUsize::new(0);
16
17#[derive(Clone)]
18pub enum ClickAction {
19 Simple(Rc<RefCell<dyn FnMut()>>),
20 WithPoint(Rc<dyn Fn(Point)>),
21}
22
23impl ClickAction {
24 fn invoke(&self, local_position: Point) {
25 match self {
26 ClickAction::Simple(handler) => (handler.borrow_mut())(),
27 ClickAction::WithPoint(handler) => handler(local_position),
28 }
29 }
30}
31
32#[derive(Clone, Copy, Debug, PartialEq)]
33pub struct HitClip {
34 pub quad: [[f32; 2]; 4],
35 pub bounds: Rect,
36}
37
38#[derive(Clone)]
39pub struct HitGeometry {
40 pub rect: Rect,
41 pub quad: [[f32; 2]; 4],
42 pub local_bounds: Rect,
43 pub world_to_local: ProjectiveTransform,
44 pub hit_clip_bounds: Option<Rect>,
45 pub hit_clips: Vec<HitClip>,
46}
47
48#[derive(Clone)]
49pub struct HitRegion {
50 pub node_id: NodeId,
51 pub capture_path: Vec<NodeId>,
52 pub rect: Rect,
53 pub quad: [[f32; 2]; 4],
54 pub local_bounds: Rect,
55 pub world_to_local: ProjectiveTransform,
56 pub shape: Option<RoundedCornerShape>,
57 pub click_actions: Vec<ClickAction>,
58 pub pointer_inputs: Vec<Rc<dyn Fn(PointerEvent)>>,
59 pub z_index: usize,
60 pub hit_clip_bounds: Option<Rect>,
61 pub hit_clips: Vec<HitClip>,
62}
63
64impl HitRegion {
65 pub fn live_modifier_slice_lookup_miss_count() -> usize {
66 LIVE_MODIFIER_SLICE_LOOKUP_MISS_COUNT.load(Ordering::Relaxed)
67 }
68
69 fn contains(&self, x: f32, y: f32) -> bool {
70 if !self.rect.contains(x, y) {
71 return false;
72 }
73
74 if let Some(clip_bounds) = self.hit_clip_bounds {
75 if !clip_bounds.contains(x, y) {
76 return false;
77 }
78 }
79
80 let point = Point { x, y };
81 if !point_in_quad(point, self.quad) {
82 return false;
83 }
84
85 for clip in &self.hit_clips {
86 if !point_in_quad(point, clip.quad) {
87 return false;
88 }
89 }
90
91 let local_point = self.world_to_local.map_point(point);
92 if let Some(shape) = self.shape {
93 point_in_rounded_rect(local_point, self.local_bounds, shape)
94 } else {
95 self.local_bounds.contains(local_point.x, local_point.y)
96 }
97 }
98
99 fn localize_event(&self, event: &PointerEvent) -> (PointerEvent, Point) {
100 let local = self.world_to_local.map_point(event.global_position);
101 let local_position = Point {
102 x: local.x - self.local_bounds.x,
103 y: local.y - self.local_bounds.y,
104 };
105 (
106 event.copy_with_local_position(local_position),
107 local_position,
108 )
109 }
110
111 fn dispatch_pointer_inputs(
112 pointer_inputs: &[Rc<dyn Fn(PointerEvent)>],
113 local_event: &PointerEvent,
114 ) {
115 for handler in pointer_inputs {
116 if local_event.is_consumed() {
117 break;
118 }
119 handler(local_event.clone());
120 }
121 }
122
123 fn dispatch_click_actions(&self, local_position: Point) {
124 for action in &self.click_actions {
125 action.invoke(local_position);
126 }
127 }
128
129 fn dispatch_modifier_slices(&self, modifier_slices: &ModifierNodeSlices, event: PointerEvent) {
130 if event.is_consumed() {
131 return;
132 }
133
134 let (local_event, local_position) = self.localize_event(&event);
135 Self::dispatch_pointer_inputs(modifier_slices.pointer_inputs(), &local_event);
136
137 if event.kind == PointerEventKind::Down && !local_event.is_consumed() {
138 for handler in modifier_slices.click_handlers() {
139 handler(local_position);
140 }
141 }
142 }
143
144 fn dispatch_cached_handlers(&self, event: PointerEvent) {
145 if event.is_consumed() {
146 return;
147 }
148
149 let (local_event, local_position) = self.localize_event(&event);
150 Self::dispatch_pointer_inputs(&self.pointer_inputs, &local_event);
151
152 if event.kind == PointerEventKind::Down && !local_event.is_consumed() {
153 self.dispatch_click_actions(local_position);
154 }
155 }
156
157 fn live_modifier_slices(&self, applier: &mut MemoryApplier) -> Option<Rc<ModifierNodeSlices>> {
158 if let Ok(modifier_slices) =
159 applier.with_node::<LayoutNode, _>(self.node_id, |node| node.modifier_slices_snapshot())
160 {
161 return Some(modifier_slices);
162 }
163
164 applier
165 .with_node::<SubcomposeLayoutNode, _>(self.node_id, |node| {
166 node.modifier_slices_snapshot()
167 })
168 .ok()
169 }
170}
171
172impl HitTestTarget for HitRegion {
173 fn node_id(&self) -> NodeId {
174 self.node_id
175 }
176
177 fn capture_path(&self) -> Vec<NodeId> {
178 self.capture_path.clone()
179 }
180
181 fn dispatch(&self, event: PointerEvent) {
182 self.dispatch_cached_handlers(event);
183 }
184
185 fn dispatch_with_applier(&self, applier: &mut MemoryApplier, event: PointerEvent) {
186 if let Some(modifier_slices) = self.live_modifier_slices(applier) {
187 self.dispatch_modifier_slices(modifier_slices.as_ref(), event);
188 return;
189 }
190
191 LIVE_MODIFIER_SLICE_LOOKUP_MISS_COUNT.fetch_add(1, Ordering::Relaxed);
192 self.dispatch_cached_handlers(event);
193 }
194}
195
196pub struct Scene {
197 pub graph: Option<RenderGraph>,
198 pub hits: Vec<HitRegion>,
199 pub next_hit_z: usize,
200 pub node_index: HashMap<NodeId, usize>,
201}
202
203impl Scene {
204 pub fn new() -> Self {
205 Self {
206 graph: None,
207 hits: Vec::new(),
208 next_hit_z: 0,
209 node_index: HashMap::new(),
210 }
211 }
212
213 pub fn push_hit(
214 &mut self,
215 node_id: NodeId,
216 capture_path: Vec<NodeId>,
217 geometry: HitGeometry,
218 shape: Option<RoundedCornerShape>,
219 click_actions: Vec<ClickAction>,
220 pointer_inputs: Vec<Rc<dyn Fn(PointerEvent)>>,
221 ) {
222 if click_actions.is_empty() && pointer_inputs.is_empty() {
223 return;
224 }
225
226 let z_index = self.next_hit_z;
227 self.next_hit_z += 1;
228 let hit_index = self.hits.len();
229 let HitGeometry {
230 rect,
231 quad,
232 local_bounds,
233 world_to_local,
234 hit_clip_bounds,
235 hit_clips,
236 } = geometry;
237 self.hits.push(HitRegion {
238 node_id,
239 capture_path,
240 rect,
241 quad,
242 local_bounds,
243 world_to_local,
244 shape,
245 click_actions,
246 pointer_inputs,
247 z_index,
248 hit_clip_bounds,
249 hit_clips,
250 });
251 self.node_index.insert(node_id, hit_index);
252 }
253
254 pub fn replace_graph(&mut self, graph: RenderGraph) {
255 self.graph = Some(graph);
256 }
257}
258
259impl Default for Scene {
260 fn default() -> Self {
261 Self::new()
262 }
263}
264
265impl RenderScene for Scene {
266 type HitTarget = HitRegion;
267
268 fn clear(&mut self) {
269 self.graph = None;
270 self.hits.clear();
271 self.node_index.clear();
272 self.next_hit_z = 0;
273 }
274
275 fn hit_test(&self, x: f32, y: f32) -> Vec<Self::HitTarget> {
276 let mut hit_indices: Vec<usize> = self
277 .hits
278 .iter()
279 .enumerate()
280 .filter_map(|(index, hit)| hit.contains(x, y).then_some(index))
281 .collect();
282
283 hit_indices.sort_by_key(|&index| Reverse(self.hits[index].z_index));
284 hit_indices
285 .into_iter()
286 .map(|index| self.hits[index].clone())
287 .collect()
288 }
289
290 fn find_target(&self, node_id: NodeId) -> Option<Self::HitTarget> {
291 self.node_index
292 .get(&node_id)
293 .and_then(|&index| self.hits.get(index))
294 .cloned()
295 }
296}
297
298fn point_in_rounded_rect(point: Point, rect: Rect, shape: RoundedCornerShape) -> bool {
299 if !rect.contains(point.x, point.y) {
300 return false;
301 }
302
303 let local_x = point.x - rect.x;
304 let local_y = point.y - rect.y;
305 let radii = shape.resolve(rect.width, rect.height);
306 let tl = radii.top_left;
307 let tr = radii.top_right;
308 let bl = radii.bottom_left;
309 let br = radii.bottom_right;
310
311 if local_x < tl && local_y < tl {
312 let dx = tl - local_x;
313 let dy = tl - local_y;
314 return dx * dx + dy * dy <= tl * tl;
315 }
316
317 if local_x > rect.width - tr && local_y < tr {
318 let dx = local_x - (rect.width - tr);
319 let dy = tr - local_y;
320 return dx * dx + dy * dy <= tr * tr;
321 }
322
323 if local_x < bl && local_y > rect.height - bl {
324 let dx = bl - local_x;
325 let dy = local_y - (rect.height - bl);
326 return dx * dx + dy * dy <= bl * bl;
327 }
328
329 if local_x > rect.width - br && local_y > rect.height - br {
330 let dx = local_x - (rect.width - br);
331 let dy = local_y - (rect.height - br);
332 return dx * dx + dy * dy <= br * br;
333 }
334
335 true
336}
337
338fn point_in_quad(point: Point, quad: [[f32; 2]; 4]) -> bool {
339 point_in_triangle(point, quad[0], quad[1], quad[3])
340 || point_in_triangle(point, quad[0], quad[3], quad[2])
341}
342
343fn point_in_triangle(point: Point, a: [f32; 2], b: [f32; 2], c: [f32; 2]) -> bool {
344 let d1 = triangle_sign(point, a, b);
345 let d2 = triangle_sign(point, b, c);
346 let d3 = triangle_sign(point, c, a);
347 let has_negative = d1 < -f32::EPSILON || d2 < -f32::EPSILON || d3 < -f32::EPSILON;
348 let has_positive = d1 > f32::EPSILON || d2 > f32::EPSILON || d3 > f32::EPSILON;
349 !(has_negative && has_positive)
350}
351
352fn triangle_sign(point: Point, a: [f32; 2], b: [f32; 2]) -> f32 {
353 (point.x - b[0]) * (a[1] - b[1]) - (a[0] - b[0]) * (point.y - b[1])
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359 use std::cell::Cell;
360
361 fn rect_to_quad(rect: Rect) -> [[f32; 2]; 4] {
362 [
363 [rect.x, rect.y],
364 [rect.x + rect.width, rect.y],
365 [rect.x, rect.y + rect.height],
366 [rect.x + rect.width, rect.y + rect.height],
367 ]
368 }
369
370 fn translated_world_to_local(rect: Rect) -> ProjectiveTransform {
371 ProjectiveTransform::translation(-rect.x, -rect.y)
372 }
373
374 fn local_bounds_for_rect(rect: Rect) -> Rect {
375 Rect {
376 x: 0.0,
377 y: 0.0,
378 width: rect.width,
379 height: rect.height,
380 }
381 }
382
383 fn hit_geometry_for_rect(rect: Rect) -> HitGeometry {
384 HitGeometry {
385 rect,
386 quad: rect_to_quad(rect),
387 local_bounds: local_bounds_for_rect(rect),
388 world_to_local: translated_world_to_local(rect),
389 hit_clip_bounds: None,
390 hit_clips: Vec::new(),
391 }
392 }
393
394 fn make_handler(counter: Rc<Cell<u32>>, consume: bool) -> Rc<dyn Fn(PointerEvent)> {
395 Rc::new(move |event: PointerEvent| {
396 counter.set(counter.get() + 1);
397 if consume {
398 event.consume();
399 }
400 })
401 }
402
403 #[test]
404 fn hit_test_respects_hit_clip() {
405 let mut scene = Scene::new();
406 let rect = Rect {
407 x: 0.0,
408 y: 0.0,
409 width: 100.0,
410 height: 100.0,
411 };
412 let clip = Rect {
413 x: 0.0,
414 y: 0.0,
415 width: 40.0,
416 height: 40.0,
417 };
418 scene.push_hit(
419 1,
420 vec![1],
421 HitGeometry {
422 hit_clip_bounds: Some(clip),
423 hit_clips: vec![HitClip {
424 quad: rect_to_quad(clip),
425 bounds: clip,
426 }],
427 ..hit_geometry_for_rect(rect)
428 },
429 None,
430 Vec::new(),
431 vec![Rc::new(|_event: PointerEvent| {})],
432 );
433
434 assert!(scene.hit_test(60.0, 20.0).is_empty());
435 assert_eq!(scene.hit_test(20.0, 20.0).len(), 1);
436 }
437
438 #[test]
439 fn hit_test_sorts_by_z_without_duplicating_hit_storage() {
440 let mut scene = Scene::new();
441 let rect = Rect {
442 x: 0.0,
443 y: 0.0,
444 width: 50.0,
445 height: 50.0,
446 };
447
448 scene.push_hit(
449 1,
450 vec![1],
451 hit_geometry_for_rect(rect),
452 None,
453 Vec::new(),
454 vec![Rc::new(|_event: PointerEvent| {})],
455 );
456 scene.push_hit(
457 2,
458 vec![2],
459 hit_geometry_for_rect(rect),
460 None,
461 Vec::new(),
462 vec![Rc::new(|_event: PointerEvent| {})],
463 );
464
465 assert_eq!(scene.node_index.get(&1), Some(&0));
466 assert_eq!(scene.node_index.get(&2), Some(&1));
467
468 let hits = scene.hit_test(10.0, 10.0);
469 assert_eq!(
470 hits.iter().map(|hit| hit.node_id).collect::<Vec<_>>(),
471 vec![2, 1]
472 );
473 assert_eq!(scene.find_target(1).map(|hit| hit.node_id), Some(1));
474 assert_eq!(scene.find_target(2).map(|hit| hit.node_id), Some(2));
475 }
476
477 #[test]
478 fn hit_test_rejects_points_in_rounded_corner_cutout() {
479 let mut scene = Scene::new();
480 let rect = Rect {
481 x: 0.0,
482 y: 0.0,
483 width: 40.0,
484 height: 40.0,
485 };
486 scene.push_hit(
487 1,
488 vec![1],
489 hit_geometry_for_rect(rect),
490 Some(RoundedCornerShape::uniform(20.0)),
491 Vec::new(),
492 vec![Rc::new(|_event: PointerEvent| {})],
493 );
494
495 assert!(scene.hit_test(1.0, 1.0).is_empty());
496 assert_eq!(scene.hit_test(20.0, 20.0).len(), 1);
497 }
498
499 #[test]
500 fn dispatch_stops_after_event_consumed() {
501 let count_first = Rc::new(Cell::new(0));
502 let count_second = Rc::new(Cell::new(0));
503
504 let hit = HitRegion {
505 node_id: 1,
506 capture_path: vec![1],
507 rect: Rect {
508 x: 0.0,
509 y: 0.0,
510 width: 50.0,
511 height: 50.0,
512 },
513 quad: rect_to_quad(Rect {
514 x: 0.0,
515 y: 0.0,
516 width: 50.0,
517 height: 50.0,
518 }),
519 local_bounds: Rect {
520 x: 0.0,
521 y: 0.0,
522 width: 50.0,
523 height: 50.0,
524 },
525 world_to_local: ProjectiveTransform::identity(),
526 shape: None,
527 click_actions: Vec::new(),
528 pointer_inputs: vec![
529 make_handler(count_first.clone(), true),
530 make_handler(count_second.clone(), false),
531 ],
532 z_index: 0,
533 hit_clip_bounds: None,
534 hit_clips: Vec::new(),
535 };
536
537 let event = PointerEvent::new(
538 PointerEventKind::Down,
539 Point { x: 10.0, y: 10.0 },
540 Point { x: 10.0, y: 10.0 },
541 );
542 hit.dispatch(event);
543
544 assert_eq!(count_first.get(), 1);
545 assert_eq!(count_second.get(), 0);
546 }
547
548 #[test]
549 fn dispatch_triggers_click_action_on_down() {
550 let click_count = Rc::new(Cell::new(0));
551 let click_count_for_handler = Rc::clone(&click_count);
552 let click_action = ClickAction::Simple(Rc::new(RefCell::new(move || {
553 click_count_for_handler.set(click_count_for_handler.get() + 1);
554 })));
555
556 let hit = HitRegion {
557 node_id: 1,
558 capture_path: vec![1],
559 rect: Rect {
560 x: 0.0,
561 y: 0.0,
562 width: 50.0,
563 height: 50.0,
564 },
565 quad: rect_to_quad(Rect {
566 x: 0.0,
567 y: 0.0,
568 width: 50.0,
569 height: 50.0,
570 }),
571 local_bounds: Rect {
572 x: 0.0,
573 y: 0.0,
574 width: 50.0,
575 height: 50.0,
576 },
577 world_to_local: ProjectiveTransform::identity(),
578 shape: None,
579 click_actions: vec![click_action],
580 pointer_inputs: Vec::new(),
581 z_index: 0,
582 hit_clip_bounds: None,
583 hit_clips: Vec::new(),
584 };
585
586 hit.dispatch(PointerEvent::new(
587 PointerEventKind::Down,
588 Point { x: 10.0, y: 10.0 },
589 Point { x: 10.0, y: 10.0 },
590 ));
591 hit.dispatch(PointerEvent::new(
592 PointerEventKind::Move,
593 Point { x: 10.0, y: 10.0 },
594 Point { x: 12.0, y: 12.0 },
595 ));
596
597 assert_eq!(click_count.get(), 1);
598 }
599
600 #[test]
601 fn dispatch_passes_local_position_to_click_action() {
602 let local_positions = Rc::new(RefCell::new(Vec::new()));
603 let local_positions_for_handler = Rc::clone(&local_positions);
604 let click_action = ClickAction::WithPoint(Rc::new(move |point| {
605 local_positions_for_handler.borrow_mut().push(point);
606 }));
607
608 let hit = HitRegion {
609 node_id: 1,
610 capture_path: vec![1],
611 rect: Rect {
612 x: 10.0,
613 y: 12.0,
614 width: 50.0,
615 height: 50.0,
616 },
617 quad: rect_to_quad(Rect {
618 x: 10.0,
619 y: 12.0,
620 width: 50.0,
621 height: 50.0,
622 }),
623 local_bounds: Rect {
624 x: 0.0,
625 y: 0.0,
626 width: 50.0,
627 height: 50.0,
628 },
629 world_to_local: ProjectiveTransform::translation(-10.0, -12.0),
630 shape: None,
631 click_actions: vec![click_action],
632 pointer_inputs: Vec::new(),
633 z_index: 0,
634 hit_clip_bounds: None,
635 hit_clips: Vec::new(),
636 };
637
638 hit.dispatch(PointerEvent::new(
639 PointerEventKind::Down,
640 Point { x: 15.0, y: 17.0 },
641 Point { x: 15.0, y: 17.0 },
642 ));
643
644 assert_eq!(*local_positions.borrow(), vec![Point { x: 5.0, y: 5.0 }]);
645 }
646
647 #[test]
648 fn dispatch_does_not_trigger_click_action_when_consumed() {
649 let click_count = Rc::new(Cell::new(0));
650 let click_count_for_handler = Rc::clone(&click_count);
651 let click_action = ClickAction::Simple(Rc::new(RefCell::new(move || {
652 click_count_for_handler.set(click_count_for_handler.get() + 1);
653 })));
654
655 let hit = HitRegion {
656 node_id: 1,
657 capture_path: vec![1],
658 rect: Rect {
659 x: 0.0,
660 y: 0.0,
661 width: 50.0,
662 height: 50.0,
663 },
664 quad: rect_to_quad(Rect {
665 x: 0.0,
666 y: 0.0,
667 width: 50.0,
668 height: 50.0,
669 }),
670 local_bounds: Rect {
671 x: 0.0,
672 y: 0.0,
673 width: 50.0,
674 height: 50.0,
675 },
676 world_to_local: ProjectiveTransform::identity(),
677 shape: None,
678 click_actions: vec![click_action],
679 pointer_inputs: vec![Rc::new(|event: PointerEvent| event.consume())],
680 z_index: 0,
681 hit_clip_bounds: None,
682 hit_clips: Vec::new(),
683 };
684
685 hit.dispatch(PointerEvent::new(
686 PointerEventKind::Down,
687 Point { x: 10.0, y: 10.0 },
688 Point { x: 10.0, y: 10.0 },
689 ));
690
691 assert_eq!(click_count.get(), 0);
692 }
693
694 #[test]
695 fn hit_test_uses_exact_quad_for_transformed_region() {
696 let mut scene = Scene::new();
697 let rect = Rect {
698 x: 0.0,
699 y: 0.0,
700 width: 40.0,
701 height: 20.0,
702 };
703 let quad = [[10.0, 10.0], [50.0, 10.0], [20.0, 30.0], [60.0, 30.0]];
704 let world_to_local = ProjectiveTransform::from_rect_to_quad(rect, quad)
705 .inverse()
706 .expect("transformed hit region should be invertible");
707 scene.push_hit(
708 1,
709 vec![1],
710 HitGeometry {
711 rect: Rect {
712 x: 10.0,
713 y: 10.0,
714 width: 50.0,
715 height: 20.0,
716 },
717 quad,
718 local_bounds: rect,
719 world_to_local,
720 hit_clip_bounds: None,
721 hit_clips: Vec::new(),
722 },
723 None,
724 Vec::new(),
725 vec![Rc::new(|_event: PointerEvent| {})],
726 );
727
728 assert!(
729 scene.hit_test(15.0, 28.0).is_empty(),
730 "point inside the quad bounds but outside the transformed quad must not hit"
731 );
732 assert_eq!(scene.hit_test(30.0, 20.0).len(), 1);
733 }
734
735 #[test]
736 fn dispatch_uses_inverse_transform_for_local_position() {
737 let local_positions = Rc::new(RefCell::new(Vec::new()));
738 let local_positions_for_handler = Rc::clone(&local_positions);
739 let click_action = ClickAction::WithPoint(Rc::new(move |point| {
740 local_positions_for_handler.borrow_mut().push(point);
741 }));
742 let local_bounds = Rect {
743 x: 0.0,
744 y: 0.0,
745 width: 20.0,
746 height: 10.0,
747 };
748 let quad = [[20.0, 10.0], [60.0, 10.0], [20.0, 30.0], [60.0, 30.0]];
749 let world_to_local = ProjectiveTransform::from_rect_to_quad(local_bounds, quad)
750 .inverse()
751 .expect("translated quad should be invertible");
752 let hit = HitRegion {
753 node_id: 1,
754 capture_path: vec![1],
755 rect: Rect {
756 x: 20.0,
757 y: 10.0,
758 width: 40.0,
759 height: 20.0,
760 },
761 quad,
762 local_bounds,
763 world_to_local,
764 shape: None,
765 click_actions: vec![click_action],
766 pointer_inputs: Vec::new(),
767 z_index: 0,
768 hit_clip_bounds: None,
769 hit_clips: Vec::new(),
770 };
771
772 hit.dispatch(PointerEvent::new(
773 PointerEventKind::Down,
774 Point { x: 25.0, y: 17.0 },
775 Point { x: 25.0, y: 17.0 },
776 ));
777
778 assert_eq!(*local_positions.borrow(), vec![Point { x: 2.5, y: 3.5 }]);
779 }
780
781 #[test]
782 fn dispatch_with_applier_counts_live_modifier_slice_lookup_misses() {
783 let handler_calls = Rc::new(Cell::new(0));
784 let handler_calls_for_handler = Rc::clone(&handler_calls);
785 let hit = HitRegion {
786 node_id: 42,
787 capture_path: vec![42],
788 rect: Rect {
789 x: 0.0,
790 y: 0.0,
791 width: 50.0,
792 height: 50.0,
793 },
794 quad: rect_to_quad(Rect {
795 x: 0.0,
796 y: 0.0,
797 width: 50.0,
798 height: 50.0,
799 }),
800 local_bounds: Rect {
801 x: 0.0,
802 y: 0.0,
803 width: 50.0,
804 height: 50.0,
805 },
806 world_to_local: ProjectiveTransform::identity(),
807 shape: None,
808 click_actions: Vec::new(),
809 pointer_inputs: vec![Rc::new(move |_event: PointerEvent| {
810 handler_calls_for_handler.set(handler_calls_for_handler.get() + 1);
811 })],
812 z_index: 0,
813 hit_clip_bounds: None,
814 hit_clips: Vec::new(),
815 };
816 let misses_before = HitRegion::live_modifier_slice_lookup_miss_count();
817 let mut applier = MemoryApplier::new();
818
819 hit.dispatch_with_applier(
820 &mut applier,
821 PointerEvent::new(
822 PointerEventKind::Down,
823 Point { x: 10.0, y: 10.0 },
824 Point { x: 10.0, y: 10.0 },
825 ),
826 );
827
828 assert_eq!(handler_calls.get(), 1);
829 assert_eq!(
830 HitRegion::live_modifier_slice_lookup_miss_count(),
831 misses_before + 1
832 );
833 }
834}