Skip to main content

cranpose_render_common/
graph_scene.rs

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}