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