Skip to main content

cranpose_render_common/
graph_scene.rs

1use std::{
2    cell::{Cell, RefCell},
3    cmp::Reverse,
4    collections::HashMap,
5    rc::Rc,
6};
7
8use cranpose_core::{MemoryApplier, NodeId, collections::map::HashSet};
9use cranpose_foundation::{PointerEvent, PointerEventKind};
10use cranpose_ui::{LayoutNode, ModifierNodeSlices, SubcomposeLayoutNode};
11use cranpose_ui_graphics::{Point, Rect, RoundedCornerShape};
12
13use crate::{
14    HitTestTarget, RenderScene,
15    graph::{ProjectiveTransform, RenderGraph},
16};
17
18pub struct RenderDiagnostics {
19    reported_warnings: RefCell<HashSet<&'static str>>,
20    live_modifier_slice_lookup_miss_count: Cell<usize>,
21}
22
23impl RenderDiagnostics {
24    pub fn new() -> Self {
25        Self {
26            reported_warnings: RefCell::new(HashSet::new()),
27            live_modifier_slice_lookup_miss_count: Cell::new(0),
28        }
29    }
30
31    pub fn claim_warning_once(&self, key: &'static str) -> bool {
32        self.reported_warnings.borrow_mut().insert(key)
33    }
34
35    pub fn record_live_modifier_slice_lookup_miss(&self) {
36        self.live_modifier_slice_lookup_miss_count.set(
37            self.live_modifier_slice_lookup_miss_count
38                .get()
39                .saturating_add(1),
40        );
41    }
42
43    pub fn live_modifier_slice_lookup_miss_count(&self) -> usize {
44        self.live_modifier_slice_lookup_miss_count.get()
45    }
46}
47
48impl Default for RenderDiagnostics {
49    fn default() -> Self {
50        Self::new()
51    }
52}
53
54#[derive(Clone)]
55pub enum ClickAction {
56    Simple(Rc<RefCell<dyn FnMut()>>),
57    WithPoint(Rc<dyn Fn(Point)>),
58}
59
60impl ClickAction {
61    fn invoke(&self, local_position: Point) {
62        match self {
63            ClickAction::Simple(handler) => (handler.borrow_mut())(),
64            ClickAction::WithPoint(handler) => handler(local_position),
65        }
66    }
67}
68
69#[derive(Clone, Copy, Debug, PartialEq)]
70pub struct HitClip {
71    pub quad: [[f32; 2]; 4],
72    pub bounds: Rect,
73}
74
75/// Geometry for a hit target, borrowing the clip chain until the sink records it.
76#[derive(Clone, Copy)]
77pub struct HitGeometry<'a> {
78    pub rect: Rect,
79    pub quad: [[f32; 2]; 4],
80    pub local_bounds: Rect,
81    pub world_to_local: ProjectiveTransform,
82    pub hit_clip_bounds: Option<Rect>,
83    pub hit_clips: &'a [HitClip],
84}
85
86#[derive(Clone)]
87pub struct HitRegion {
88    pub node_id: NodeId,
89    pub capture_path: Vec<NodeId>,
90    pub rect: Rect,
91    pub quad: [[f32; 2]; 4],
92    pub local_bounds: Rect,
93    pub world_to_local: ProjectiveTransform,
94    pub shape: Option<RoundedCornerShape>,
95    pub click_actions: Vec<ClickAction>,
96    pub pointer_inputs: Vec<Rc<dyn Fn(PointerEvent)>>,
97    pub z_index: usize,
98    pub hit_clip_bounds: Option<Rect>,
99    pub hit_clips: Vec<HitClip>,
100    diagnostics: Rc<RenderDiagnostics>,
101}
102
103struct HitRegionInit<'a> {
104    node_id: NodeId,
105    capture_path: Vec<NodeId>,
106    geometry: HitGeometry<'a>,
107    clip_buffer: Vec<HitClip>,
108    shape: Option<RoundedCornerShape>,
109    click_actions: Vec<ClickAction>,
110    pointer_inputs: Vec<Rc<dyn Fn(PointerEvent)>>,
111    z_index: usize,
112    diagnostics: Rc<RenderDiagnostics>,
113}
114
115impl Default for HitRegionInit<'_> {
116    fn default() -> Self {
117        Self {
118            node_id: 0,
119            capture_path: Vec::new(),
120            geometry: HitGeometry {
121                rect: Rect {
122                    x: 0.0,
123                    y: 0.0,
124                    width: 0.0,
125                    height: 0.0,
126                },
127                quad: [[0.0, 0.0]; 4],
128                local_bounds: Rect {
129                    x: 0.0,
130                    y: 0.0,
131                    width: 0.0,
132                    height: 0.0,
133                },
134                world_to_local: ProjectiveTransform::identity(),
135                hit_clip_bounds: None,
136                hit_clips: &[],
137            },
138            clip_buffer: Vec::new(),
139            shape: None,
140            click_actions: Vec::new(),
141            pointer_inputs: Vec::new(),
142            z_index: 0,
143            diagnostics: Rc::new(RenderDiagnostics::new()),
144        }
145    }
146}
147
148impl HitRegion {
149    fn with_diagnostics(init: HitRegionInit<'_>) -> Self {
150        let HitRegionInit {
151            node_id,
152            capture_path,
153            geometry,
154            clip_buffer: mut hit_clips,
155            shape,
156            click_actions,
157            pointer_inputs,
158            z_index,
159            diagnostics,
160        } = init;
161        let HitGeometry {
162            rect,
163            quad,
164            local_bounds,
165            world_to_local,
166            hit_clip_bounds,
167            hit_clips: clips,
168        } = geometry;
169        hit_clips.extend_from_slice(clips);
170        Self {
171            node_id,
172            capture_path,
173            rect,
174            quad,
175            local_bounds,
176            world_to_local,
177            shape,
178            click_actions,
179            pointer_inputs,
180            z_index,
181            hit_clip_bounds,
182            hit_clips,
183            diagnostics,
184        }
185    }
186
187    fn contains(&self, x: f32, y: f32) -> bool {
188        if !self.rect.contains(x, y) {
189            return false;
190        }
191
192        if let Some(clip_bounds) = self.hit_clip_bounds
193            && !clip_bounds.contains(x, y)
194        {
195            return false;
196        }
197
198        let point = Point { x, y };
199        if !point_in_quad(point, self.quad) {
200            return false;
201        }
202
203        for clip in &self.hit_clips {
204            if !point_in_quad(point, clip.quad) {
205                return false;
206            }
207        }
208
209        let local_point = self.world_to_local.map_point(point);
210        if let Some(shape) = self.shape {
211            point_in_rounded_rect(local_point, self.local_bounds, shape)
212        } else {
213            self.local_bounds.contains(local_point.x, local_point.y)
214        }
215    }
216
217    fn localize_event(&self, event: &PointerEvent) -> (PointerEvent, Point) {
218        let local = self.world_to_local.map_point(event.global_position);
219        let local_position = Point {
220            x: local.x - self.local_bounds.x,
221            y: local.y - self.local_bounds.y,
222        };
223        (
224            event.copy_with_local_position(local_position),
225            local_position,
226        )
227    }
228
229    fn dispatch_pointer_inputs(
230        pointer_inputs: &[Rc<dyn Fn(PointerEvent)>],
231        local_event: &PointerEvent,
232    ) {
233        for handler in pointer_inputs {
234            if local_event.is_consumed() && !is_terminal_pointer_event(local_event.kind) {
235                break;
236            }
237            handler(local_event.clone());
238        }
239    }
240
241    fn dispatch_click_actions(&self, local_position: Point) {
242        for action in &self.click_actions {
243            action.invoke(local_position);
244        }
245    }
246
247    fn dispatch_modifier_slices(&self, modifier_slices: &ModifierNodeSlices, event: PointerEvent) {
248        if should_skip_consumed_event(&event) {
249            return;
250        }
251
252        let (local_event, local_position) = self.localize_event(&event);
253        Self::dispatch_pointer_inputs(modifier_slices.pointer_inputs(), &local_event);
254
255        if event.kind == PointerEventKind::Down && !local_event.is_consumed() {
256            for handler in modifier_slices.click_handlers() {
257                handler(local_position);
258            }
259        }
260    }
261
262    fn dispatch_cached_handlers(&self, event: PointerEvent) {
263        if should_skip_consumed_event(&event) {
264            return;
265        }
266
267        let (local_event, local_position) = self.localize_event(&event);
268        Self::dispatch_pointer_inputs(&self.pointer_inputs, &local_event);
269
270        if event.kind == PointerEventKind::Down && !local_event.is_consumed() {
271            self.dispatch_click_actions(local_position);
272        }
273    }
274
275    fn live_modifier_slices(&self, applier: &mut MemoryApplier) -> Option<Rc<ModifierNodeSlices>> {
276        if let Ok(modifier_slices) =
277            applier.with_node::<LayoutNode, _>(self.node_id, |node| node.modifier_slices_snapshot())
278        {
279            return Some(modifier_slices);
280        }
281
282        applier
283            .with_node::<SubcomposeLayoutNode, _>(self.node_id, |node| {
284                node.modifier_slices_snapshot()
285            })
286            .ok()
287    }
288}
289
290fn is_terminal_pointer_event(kind: PointerEventKind) -> bool {
291    matches!(kind, PointerEventKind::Up | PointerEventKind::Cancel)
292}
293
294fn should_skip_consumed_event(event: &PointerEvent) -> bool {
295    event.is_consumed() && !is_terminal_pointer_event(event.kind)
296}
297
298impl HitTestTarget for HitRegion {
299    fn node_id(&self) -> NodeId {
300        self.node_id
301    }
302
303    fn capture_path(&self) -> Vec<NodeId> {
304        self.capture_path.clone()
305    }
306
307    fn dispatch(&self, event: PointerEvent) {
308        self.dispatch_cached_handlers(event);
309    }
310
311    fn dispatch_with_applier(&self, applier: &mut MemoryApplier, event: PointerEvent) {
312        if let Some(modifier_slices) = self.live_modifier_slices(applier) {
313            self.dispatch_modifier_slices(modifier_slices.as_ref(), event);
314            return;
315        }
316
317        self.diagnostics.record_live_modifier_slice_lookup_miss();
318        self.dispatch_cached_handlers(event);
319    }
320}
321
322#[derive(Default)]
323struct HitBuffers {
324    hit_clips: Vec<HitClip>,
325    capture_path: Vec<NodeId>,
326    click_actions: Vec<ClickAction>,
327    pointer_inputs: Vec<Rc<dyn Fn(PointerEvent)>>,
328}
329
330pub struct Scene {
331    pub graph: Option<RenderGraph>,
332    pub hits: Vec<HitRegion>,
333    hit_buffers: Vec<HitBuffers>,
334    pub next_hit_z: usize,
335    pub node_index: HashMap<NodeId, usize>,
336    diagnostics: Rc<RenderDiagnostics>,
337}
338
339impl Scene {
340    pub fn new() -> Self {
341        Self {
342            graph: None,
343            hits: Vec::new(),
344            hit_buffers: Vec::new(),
345            next_hit_z: 0,
346            node_index: HashMap::new(),
347            diagnostics: Rc::new(RenderDiagnostics::new()),
348        }
349    }
350
351    pub fn diagnostics(&self) -> &RenderDiagnostics {
352        self.diagnostics.as_ref()
353    }
354
355    /// Adds an interactive target in draw order, ignoring targets without handlers.
356    pub fn push_hit(
357        &mut self,
358        node_id: NodeId,
359        capture_path: &[NodeId],
360        geometry: HitGeometry<'_>,
361        shape: Option<RoundedCornerShape>,
362        click_actions: impl IntoIterator<Item = ClickAction>,
363        pointer_inputs: &[Rc<dyn Fn(PointerEvent)>],
364    ) {
365        let mut click_actions = click_actions.into_iter().peekable();
366        if click_actions.peek().is_none() && pointer_inputs.is_empty() {
367            return;
368        }
369        let mut buffers = self.hit_buffers.pop().unwrap_or_default();
370        buffers.capture_path.extend_from_slice(capture_path);
371        buffers.click_actions.extend(click_actions);
372        buffers.pointer_inputs.extend_from_slice(pointer_inputs);
373
374        let z_index = self.next_hit_z;
375        self.next_hit_z += 1;
376        let hit_index = self.hits.len();
377        self.hits.push(HitRegion::with_diagnostics(HitRegionInit {
378            node_id,
379            capture_path: buffers.capture_path,
380            geometry,
381            clip_buffer: buffers.hit_clips,
382            shape,
383            click_actions: buffers.click_actions,
384            pointer_inputs: buffers.pointer_inputs,
385            z_index,
386            diagnostics: Rc::clone(&self.diagnostics),
387        }));
388        self.node_index.insert(node_id, hit_index);
389    }
390
391    /// Removes all hit targets and releases their handlers while retaining vector capacity.
392    pub fn clear_hits(&mut self) {
393        self.hit_buffers.clear();
394        for hit in self.hits.drain(..) {
395            let mut buffers = HitBuffers {
396                hit_clips: hit.hit_clips,
397                capture_path: hit.capture_path,
398                click_actions: hit.click_actions,
399                pointer_inputs: hit.pointer_inputs,
400            };
401            buffers.hit_clips.clear();
402            buffers.capture_path.clear();
403            buffers.click_actions.clear();
404            buffers.pointer_inputs.clear();
405            self.hit_buffers.push(buffers);
406        }
407        self.node_index.clear();
408        self.next_hit_z = 0;
409    }
410
411    pub fn replace_graph(&mut self, graph: RenderGraph) {
412        self.graph = Some(graph);
413    }
414}
415
416impl Default for Scene {
417    fn default() -> Self {
418        Self::new()
419    }
420}
421
422impl RenderScene for Scene {
423    type HitTarget = HitRegion;
424
425    fn clear(&mut self) {
426        self.graph = None;
427        self.clear_hits();
428    }
429
430    fn hit_test(&self, x: f32, y: f32) -> Vec<Self::HitTarget> {
431        let mut hit_indices: Vec<usize> = self
432            .hits
433            .iter()
434            .enumerate()
435            .filter_map(|(index, hit)| hit.contains(x, y).then_some(index))
436            .collect();
437
438        hit_indices.sort_by_key(|&index| Reverse(self.hits[index].z_index));
439        hit_indices
440            .into_iter()
441            .map(|index| self.hits[index].clone())
442            .collect()
443    }
444
445    fn find_target(&self, node_id: NodeId) -> Option<Self::HitTarget> {
446        self.node_index
447            .get(&node_id)
448            .and_then(|&index| self.hits.get(index))
449            .cloned()
450    }
451
452    fn collect_retained_visual_observation_nodes(&self, nodes: &mut HashSet<NodeId>) -> bool {
453        if let Some(graph) = &self.graph {
454            graph.collect_retained_visual_observation_nodes(nodes);
455        } else {
456            nodes.clear();
457        }
458        true
459    }
460}
461
462fn point_in_rounded_rect(point: Point, rect: Rect, shape: RoundedCornerShape) -> bool {
463    if !rect.contains(point.x, point.y) {
464        return false;
465    }
466
467    let local_x = point.x - rect.x;
468    let local_y = point.y - rect.y;
469    let radii = shape.resolve(rect.width, rect.height);
470    let tl = radii.top_left;
471    let tr = radii.top_right;
472    let bl = radii.bottom_left;
473    let br = radii.bottom_right;
474
475    if local_x < tl && local_y < tl {
476        let dx = tl - local_x;
477        let dy = tl - local_y;
478        return dx * dx + dy * dy <= tl * tl;
479    }
480
481    if local_x > rect.width - tr && local_y < tr {
482        let dx = local_x - (rect.width - tr);
483        let dy = tr - local_y;
484        return dx * dx + dy * dy <= tr * tr;
485    }
486
487    if local_x < bl && local_y > rect.height - bl {
488        let dx = bl - local_x;
489        let dy = local_y - (rect.height - bl);
490        return dx * dx + dy * dy <= bl * bl;
491    }
492
493    if local_x > rect.width - br && local_y > rect.height - br {
494        let dx = local_x - (rect.width - br);
495        let dy = local_y - (rect.height - br);
496        return dx * dx + dy * dy <= br * br;
497    }
498
499    true
500}
501
502fn point_in_quad(point: Point, quad: [[f32; 2]; 4]) -> bool {
503    point_in_triangle(point, quad[0], quad[1], quad[3])
504        || point_in_triangle(point, quad[0], quad[3], quad[2])
505}
506
507fn point_in_triangle(point: Point, a: [f32; 2], b: [f32; 2], c: [f32; 2]) -> bool {
508    let d1 = triangle_sign(point, a, b);
509    let d2 = triangle_sign(point, b, c);
510    let d3 = triangle_sign(point, c, a);
511    let has_negative = d1 < -f32::EPSILON || d2 < -f32::EPSILON || d3 < -f32::EPSILON;
512    let has_positive = d1 > f32::EPSILON || d2 > f32::EPSILON || d3 > f32::EPSILON;
513    !(has_negative && has_positive)
514}
515
516fn triangle_sign(point: Point, a: [f32; 2], b: [f32; 2]) -> f32 {
517    (point.x - b[0]) * (a[1] - b[1]) - (a[0] - b[0]) * (point.y - b[1])
518}
519
520#[cfg(test)]
521mod tests {
522    use std::cell::Cell;
523
524    use super::*;
525
526    fn rect_to_quad(rect: Rect) -> [[f32; 2]; 4] {
527        [
528            [rect.x, rect.y],
529            [rect.x + rect.width, rect.y],
530            [rect.x, rect.y + rect.height],
531            [rect.x + rect.width, rect.y + rect.height],
532        ]
533    }
534
535    fn translated_world_to_local(rect: Rect) -> ProjectiveTransform {
536        ProjectiveTransform::translation(-rect.x, -rect.y)
537    }
538
539    fn local_bounds_for_rect(rect: Rect) -> Rect {
540        Rect {
541            x: 0.0,
542            y: 0.0,
543            width: rect.width,
544            height: rect.height,
545        }
546    }
547
548    fn hit_geometry_for_rect(rect: Rect) -> HitGeometry<'static> {
549        HitGeometry {
550            rect,
551            quad: rect_to_quad(rect),
552            local_bounds: local_bounds_for_rect(rect),
553            world_to_local: translated_world_to_local(rect),
554            hit_clip_bounds: None,
555            hit_clips: &[],
556        }
557    }
558
559    fn test_diagnostics() -> Rc<RenderDiagnostics> {
560        Rc::new(RenderDiagnostics::new())
561    }
562
563    fn make_handler(counter: Rc<Cell<u32>>, consume: bool) -> Rc<dyn Fn(PointerEvent)> {
564        Rc::new(move |event: PointerEvent| {
565            counter.set(counter.get() + 1);
566            if consume {
567                event.consume();
568            }
569        })
570    }
571
572    #[test]
573    fn rebuilding_hits_reuses_buffers_releases_handlers_and_preserves_captured_targets() {
574        let mut scene = Scene::new();
575        let first_count = Rc::new(Cell::new(0));
576        let second_count = Rc::new(Cell::new(0));
577        let first = make_handler(Rc::clone(&first_count), false);
578        let second = make_handler(Rc::clone(&second_count), false);
579        let first_click: Rc<dyn Fn(Point)> = Rc::new(|_| {});
580        let second_click: Rc<dyn Fn(Point)> = Rc::new(|_| {});
581        let rect = Rect {
582            x: 0.0,
583            y: 0.0,
584            width: 30.0,
585            height: 30.0,
586        };
587        scene.push_hit(
588            1,
589            &[1, 9],
590            hit_geometry_for_rect(rect),
591            None,
592            [ClickAction::WithPoint(Rc::clone(&first_click))],
593            &[Rc::clone(&first)],
594        );
595        let captured = scene.find_target(1).unwrap();
596        let path_storage = scene.hits[0].capture_path.as_ptr();
597        let input_storage = scene.hits[0].pointer_inputs.as_ptr();
598        scene.clear_hits();
599        assert!(scene.hits.is_empty());
600        assert!(scene.find_target(1).is_none());
601        assert_eq!(scene.next_hit_z, 0);
602        assert_eq!(Rc::strong_count(&first), 2);
603        assert_eq!(Rc::strong_count(&first_click), 2);
604        scene.push_hit(
605            2,
606            &[2],
607            hit_geometry_for_rect(rect),
608            None,
609            [ClickAction::WithPoint(Rc::clone(&second_click))],
610            &[Rc::clone(&second)],
611        );
612        assert_eq!(scene.hits[0].capture_path.as_ptr(), path_storage);
613        assert_eq!(scene.hits[0].pointer_inputs.as_ptr(), input_storage);
614        assert_eq!(scene.hits[0].capture_path, [2]);
615        assert_eq!(scene.hits[0].z_index, 0);
616        assert_eq!(scene.hits[0].click_actions.len(), 1);
617        assert!(
618            matches!(&scene.hits[0].click_actions[0], ClickAction::WithPoint(handler) if Rc::ptr_eq(handler, &second_click))
619        );
620        let event = PointerEvent::new(
621            PointerEventKind::Down,
622            Point::new(5.0, 5.0),
623            Point::new(5.0, 5.0),
624        );
625        scene.find_target(2).unwrap().dispatch(event.clone());
626        assert_eq!(first_count.get(), 0);
627        assert_eq!(second_count.get(), 1);
628        captured.dispatch(event);
629        assert_eq!(captured.capture_path, [1, 9]);
630        assert_eq!(first_count.get(), 1);
631        scene.clear_hits();
632        assert_eq!(Rc::strong_count(&second), 1);
633        assert_eq!(Rc::strong_count(&second_click), 1);
634        scene.push_hit(3, &[3], hit_geometry_for_rect(rect), None, [], &[]);
635        assert!(scene.hits.is_empty());
636        assert_eq!(scene.next_hit_z, 0);
637    }
638
639    #[test]
640    fn collecting_observation_owners_clears_an_empty_scene() {
641        let scene = Scene::new();
642        let mut nodes = HashSet::from([13, 17]);
643        let capacity = nodes.capacity();
644        assert!(scene.collect_retained_visual_observation_nodes(&mut nodes));
645        assert!(nodes.is_empty());
646        assert_eq!(nodes.capacity(), capacity);
647    }
648
649    #[test]
650    fn rebuilding_clip_buffers_replaces_clips_without_changing_captured_targets() {
651        let mut scene = Scene::new();
652        let rect = Rect {
653            x: 0.0,
654            y: 0.0,
655            width: 100.0,
656            height: 100.0,
657        };
658        let left = Rect {
659            width: 50.0,
660            ..rect
661        };
662        let right = Rect {
663            x: 50.0,
664            width: 50.0,
665            ..rect
666        };
667        let clip = |bounds| HitClip {
668            quad: rect_to_quad(bounds),
669            bounds,
670        };
671        let handler = make_handler(Rc::new(Cell::new(0)), false);
672        let push = |scene: &mut Scene, clips: &[HitClip]| {
673            scene.push_hit(
674                1,
675                &[1],
676                HitGeometry {
677                    hit_clips: clips,
678                    ..hit_geometry_for_rect(rect)
679                },
680                None,
681                [],
682                &[Rc::clone(&handler)],
683            );
684        };
685        push(&mut scene, &[clip(left)]);
686        let captured = scene.find_target(1).unwrap();
687        let storage = scene.hits[0].hit_clips.as_ptr();
688        assert!(captured.contains(25.0, 25.0));
689        assert!(!captured.contains(75.0, 25.0));
690        scene.clear_hits();
691        push(&mut scene, &[clip(right)]);
692        assert_eq!(scene.hits[0].hit_clips.as_ptr(), storage);
693        assert!(scene.hit_test(25.0, 25.0).is_empty());
694        assert_eq!(scene.hit_test(75.0, 25.0).len(), 1);
695        assert!(captured.contains(25.0, 25.0));
696        assert!(!captured.contains(75.0, 25.0));
697        scene.clear_hits();
698        push(&mut scene, &[]);
699        assert_eq!(scene.hit_test(25.0, 25.0).len(), 1);
700        assert_eq!(scene.hit_test(75.0, 25.0).len(), 1);
701    }
702
703    #[test]
704    fn hit_test_respects_hit_clip() {
705        let mut scene = Scene::new();
706        let rect = Rect {
707            x: 0.0,
708            y: 0.0,
709            width: 100.0,
710            height: 100.0,
711        };
712        let clip = Rect {
713            x: 0.0,
714            y: 0.0,
715            width: 40.0,
716            height: 40.0,
717        };
718        scene.push_hit(
719            1,
720            &[1],
721            HitGeometry {
722                hit_clip_bounds: Some(clip),
723                hit_clips: &[HitClip {
724                    quad: rect_to_quad(clip),
725                    bounds: clip,
726                }],
727                ..hit_geometry_for_rect(rect)
728            },
729            None,
730            Vec::new(),
731            &[Rc::new(|_event: PointerEvent| {})],
732        );
733
734        assert!(scene.hit_test(60.0, 20.0).is_empty());
735        assert_eq!(scene.hit_test(20.0, 20.0).len(), 1);
736    }
737
738    #[test]
739    fn hit_test_sorts_by_z_without_duplicating_hit_storage() {
740        let mut scene = Scene::new();
741        let rect = Rect {
742            x: 0.0,
743            y: 0.0,
744            width: 50.0,
745            height: 50.0,
746        };
747
748        scene.push_hit(
749            1,
750            &[1],
751            hit_geometry_for_rect(rect),
752            None,
753            Vec::new(),
754            &[Rc::new(|_event: PointerEvent| {})],
755        );
756        scene.push_hit(
757            2,
758            &[2],
759            hit_geometry_for_rect(rect),
760            None,
761            Vec::new(),
762            &[Rc::new(|_event: PointerEvent| {})],
763        );
764
765        assert_eq!(scene.node_index.get(&1), Some(&0));
766        assert_eq!(scene.node_index.get(&2), Some(&1));
767
768        let hits = scene.hit_test(10.0, 10.0);
769        assert_eq!(
770            hits.iter().map(|hit| hit.node_id).collect::<Vec<_>>(),
771            vec![2, 1]
772        );
773        assert_eq!(scene.find_target(1).map(|hit| hit.node_id), Some(1));
774        assert_eq!(scene.find_target(2).map(|hit| hit.node_id), Some(2));
775    }
776
777    #[test]
778    fn hit_test_rejects_points_in_rounded_corner_cutout() {
779        let mut scene = Scene::new();
780        let rect = Rect {
781            x: 0.0,
782            y: 0.0,
783            width: 40.0,
784            height: 40.0,
785        };
786        scene.push_hit(
787            1,
788            &[1],
789            hit_geometry_for_rect(rect),
790            Some(RoundedCornerShape::uniform(20.0)),
791            Vec::new(),
792            &[Rc::new(|_event: PointerEvent| {})],
793        );
794
795        assert!(scene.hit_test(1.0, 1.0).is_empty());
796        assert_eq!(scene.hit_test(20.0, 20.0).len(), 1);
797    }
798
799    #[test]
800    fn render_diagnostics_claim_each_warning_key_once() {
801        let diagnostics = RenderDiagnostics::new();
802
803        assert!(diagnostics.claim_warning_once("pixels.effect-fallback"));
804        assert!(!diagnostics.claim_warning_once("pixels.effect-fallback"));
805        assert!(diagnostics.claim_warning_once("pixels.blend-fallback"));
806    }
807
808    #[test]
809    fn dispatch_stops_after_event_consumed() {
810        let count_first = Rc::new(Cell::new(0));
811        let count_second = Rc::new(Cell::new(0));
812
813        let hit = HitRegion::with_diagnostics(HitRegionInit {
814            node_id: 1,
815            capture_path: vec![1],
816            geometry: hit_geometry_for_rect(Rect {
817                x: 0.0,
818                y: 0.0,
819                width: 50.0,
820                height: 50.0,
821            }),
822            pointer_inputs: vec![
823                make_handler(count_first.clone(), true),
824                make_handler(count_second.clone(), false),
825            ],
826            ..Default::default()
827        });
828
829        let event = PointerEvent::new(
830            PointerEventKind::Down,
831            Point { x: 10.0, y: 10.0 },
832            Point { x: 10.0, y: 10.0 },
833        );
834        hit.dispatch(event);
835
836        assert_eq!(count_first.get(), 1);
837        assert_eq!(count_second.get(), 0);
838    }
839
840    #[test]
841    fn dispatch_delivers_terminal_events_after_consumption_for_cleanup() {
842        let count_first = Rc::new(Cell::new(0));
843        let count_second = Rc::new(Cell::new(0));
844
845        let hit = HitRegion::with_diagnostics(HitRegionInit {
846            node_id: 1,
847            capture_path: vec![1],
848            geometry: hit_geometry_for_rect(Rect {
849                x: 0.0,
850                y: 0.0,
851                width: 50.0,
852                height: 50.0,
853            }),
854            pointer_inputs: vec![
855                make_handler(count_first.clone(), true),
856                make_handler(count_second.clone(), false),
857            ],
858            ..Default::default()
859        });
860
861        for kind in [PointerEventKind::Up, PointerEventKind::Cancel] {
862            let event =
863                PointerEvent::new(kind, Point { x: 10.0, y: 10.0 }, Point { x: 10.0, y: 10.0 });
864            hit.dispatch(event);
865        }
866
867        assert_eq!(count_first.get(), 2);
868        assert_eq!(count_second.get(), 2);
869    }
870
871    #[test]
872    fn dispatch_delivers_terminal_events_to_later_captured_targets_after_consumption() {
873        let child_count = Rc::new(Cell::new(0));
874        let parent_count = Rc::new(Cell::new(0));
875
876        let child_hit = HitRegion::with_diagnostics(HitRegionInit {
877            node_id: 2,
878            capture_path: vec![2, 1],
879            geometry: hit_geometry_for_rect(Rect {
880                x: 8.0,
881                y: 8.0,
882                width: 20.0,
883                height: 20.0,
884            }),
885            pointer_inputs: vec![make_handler(child_count.clone(), true)],
886            z_index: 1,
887            ..Default::default()
888        });
889        let parent_hit = HitRegion::with_diagnostics(HitRegionInit {
890            node_id: 1,
891            capture_path: vec![1],
892            geometry: hit_geometry_for_rect(Rect {
893                x: 0.0,
894                y: 0.0,
895                width: 50.0,
896                height: 50.0,
897            }),
898            pointer_inputs: vec![make_handler(parent_count.clone(), false)],
899            ..Default::default()
900        });
901
902        let event = PointerEvent::new(
903            PointerEventKind::Up,
904            Point { x: 12.0, y: 12.0 },
905            Point { x: 12.0, y: 12.0 },
906        );
907        child_hit.dispatch(event.clone());
908        parent_hit.dispatch(event);
909
910        assert_eq!(child_count.get(), 1);
911        assert_eq!(parent_count.get(), 1);
912    }
913
914    #[test]
915    fn dispatch_triggers_click_action_on_down() {
916        let click_count = Rc::new(Cell::new(0));
917        let click_count_for_handler = Rc::clone(&click_count);
918        let click_action = ClickAction::Simple(Rc::new(RefCell::new(move || {
919            click_count_for_handler.set(click_count_for_handler.get() + 1);
920        })));
921
922        let hit = HitRegion::with_diagnostics(HitRegionInit {
923            node_id: 1,
924            capture_path: vec![1],
925            geometry: hit_geometry_for_rect(Rect {
926                x: 0.0,
927                y: 0.0,
928                width: 50.0,
929                height: 50.0,
930            }),
931            click_actions: vec![click_action],
932            ..Default::default()
933        });
934
935        hit.dispatch(PointerEvent::new(
936            PointerEventKind::Down,
937            Point { x: 10.0, y: 10.0 },
938            Point { x: 10.0, y: 10.0 },
939        ));
940        hit.dispatch(PointerEvent::new(
941            PointerEventKind::Move,
942            Point { x: 10.0, y: 10.0 },
943            Point { x: 12.0, y: 12.0 },
944        ));
945
946        assert_eq!(click_count.get(), 1);
947    }
948
949    #[test]
950    fn dispatch_passes_local_position_to_click_action() {
951        let local_positions = Rc::new(RefCell::new(Vec::new()));
952        let local_positions_for_handler = Rc::clone(&local_positions);
953        let click_action = ClickAction::WithPoint(Rc::new(move |point| {
954            local_positions_for_handler.borrow_mut().push(point);
955        }));
956
957        let hit = HitRegion::with_diagnostics(HitRegionInit {
958            node_id: 1,
959            capture_path: vec![1],
960            geometry: hit_geometry_for_rect(Rect {
961                x: 10.0,
962                y: 12.0,
963                width: 50.0,
964                height: 50.0,
965            }),
966            click_actions: vec![click_action],
967            ..Default::default()
968        });
969
970        hit.dispatch(PointerEvent::new(
971            PointerEventKind::Down,
972            Point { x: 15.0, y: 17.0 },
973            Point { x: 15.0, y: 17.0 },
974        ));
975
976        assert_eq!(*local_positions.borrow(), vec![Point { x: 5.0, y: 5.0 }]);
977    }
978
979    #[test]
980    fn dispatch_does_not_trigger_click_action_when_consumed() {
981        let click_count = Rc::new(Cell::new(0));
982        let click_count_for_handler = Rc::clone(&click_count);
983        let click_action = ClickAction::Simple(Rc::new(RefCell::new(move || {
984            click_count_for_handler.set(click_count_for_handler.get() + 1);
985        })));
986
987        let hit = HitRegion::with_diagnostics(HitRegionInit {
988            node_id: 1,
989            capture_path: vec![1],
990            geometry: hit_geometry_for_rect(Rect {
991                x: 0.0,
992                y: 0.0,
993                width: 50.0,
994                height: 50.0,
995            }),
996            click_actions: vec![click_action],
997            pointer_inputs: vec![Rc::new(|event: PointerEvent| event.consume())],
998            ..Default::default()
999        });
1000
1001        hit.dispatch(PointerEvent::new(
1002            PointerEventKind::Down,
1003            Point { x: 10.0, y: 10.0 },
1004            Point { x: 10.0, y: 10.0 },
1005        ));
1006
1007        assert_eq!(click_count.get(), 0);
1008    }
1009
1010    #[test]
1011    fn hit_test_uses_exact_quad_for_transformed_region() {
1012        let mut scene = Scene::new();
1013        let rect = Rect {
1014            x: 0.0,
1015            y: 0.0,
1016            width: 40.0,
1017            height: 20.0,
1018        };
1019        let quad = [[10.0, 10.0], [50.0, 10.0], [20.0, 30.0], [60.0, 30.0]];
1020        let world_to_local = ProjectiveTransform::from_rect_to_quad(rect, quad)
1021            .inverse()
1022            .expect("transformed hit region should be invertible");
1023        scene.push_hit(
1024            1,
1025            &[1],
1026            HitGeometry {
1027                rect: Rect {
1028                    x: 10.0,
1029                    y: 10.0,
1030                    width: 50.0,
1031                    height: 20.0,
1032                },
1033                quad,
1034                local_bounds: rect,
1035                world_to_local,
1036                hit_clip_bounds: None,
1037                hit_clips: &[],
1038            },
1039            None,
1040            Vec::new(),
1041            &[Rc::new(|_event: PointerEvent| {})],
1042        );
1043
1044        assert!(
1045            scene.hit_test(15.0, 28.0).is_empty(),
1046            "point inside the quad bounds but outside the transformed quad must not hit"
1047        );
1048        assert_eq!(scene.hit_test(30.0, 20.0).len(), 1);
1049    }
1050
1051    #[test]
1052    fn dispatch_uses_inverse_transform_for_local_position() {
1053        let local_positions = Rc::new(RefCell::new(Vec::new()));
1054        let local_positions_for_handler = Rc::clone(&local_positions);
1055        let click_action = ClickAction::WithPoint(Rc::new(move |point| {
1056            local_positions_for_handler.borrow_mut().push(point);
1057        }));
1058        let local_bounds = Rect {
1059            x: 0.0,
1060            y: 0.0,
1061            width: 20.0,
1062            height: 10.0,
1063        };
1064        let quad = [[20.0, 10.0], [60.0, 10.0], [20.0, 30.0], [60.0, 30.0]];
1065        let world_to_local = ProjectiveTransform::from_rect_to_quad(local_bounds, quad)
1066            .inverse()
1067            .expect("translated quad should be invertible");
1068        let hit = HitRegion::with_diagnostics(HitRegionInit {
1069            node_id: 1,
1070            capture_path: vec![1],
1071            geometry: HitGeometry {
1072                rect: Rect {
1073                    x: 20.0,
1074                    y: 10.0,
1075                    width: 40.0,
1076                    height: 20.0,
1077                },
1078                quad,
1079                local_bounds,
1080                world_to_local,
1081                hit_clip_bounds: None,
1082                hit_clips: &[],
1083            },
1084            click_actions: vec![click_action],
1085            ..Default::default()
1086        });
1087
1088        hit.dispatch(PointerEvent::new(
1089            PointerEventKind::Down,
1090            Point { x: 25.0, y: 17.0 },
1091            Point { x: 25.0, y: 17.0 },
1092        ));
1093
1094        assert_eq!(*local_positions.borrow(), vec![Point { x: 2.5, y: 3.5 }]);
1095    }
1096
1097    #[test]
1098    fn dispatch_with_applier_counts_live_modifier_slice_lookup_misses() {
1099        let handler_calls = Rc::new(Cell::new(0));
1100        let handler_calls_for_handler = Rc::clone(&handler_calls);
1101        let diagnostics = test_diagnostics();
1102        let hit = HitRegion::with_diagnostics(HitRegionInit {
1103            node_id: 42,
1104            capture_path: vec![42],
1105            geometry: hit_geometry_for_rect(Rect {
1106                x: 0.0,
1107                y: 0.0,
1108                width: 50.0,
1109                height: 50.0,
1110            }),
1111            pointer_inputs: vec![Rc::new(move |_event: PointerEvent| {
1112                handler_calls_for_handler.set(handler_calls_for_handler.get() + 1);
1113            })],
1114            diagnostics: Rc::clone(&diagnostics),
1115            ..Default::default()
1116        });
1117        let misses_before = diagnostics.live_modifier_slice_lookup_miss_count();
1118        let mut applier = MemoryApplier::new();
1119
1120        hit.dispatch_with_applier(
1121            &mut applier,
1122            PointerEvent::new(
1123                PointerEventKind::Down,
1124                Point { x: 10.0, y: 10.0 },
1125                Point { x: 10.0, y: 10.0 },
1126            ),
1127        );
1128
1129        assert_eq!(handler_calls.get(), 1);
1130        assert_eq!(
1131            diagnostics.live_modifier_slice_lookup_miss_count(),
1132            misses_before + 1
1133        );
1134    }
1135}