Skip to main content

cranpose_render_common/
hit_graph.rs

1use std::rc::Rc;
2
3use cranpose_core::NodeId;
4use cranpose_foundation::PointerEvent;
5use cranpose_ui::Point;
6use cranpose_ui_graphics::{Rect, RoundedCornerShape};
7
8use crate::graph::quad_bounds;
9use crate::graph::{LayerNode, ProjectiveTransform, RenderNode};
10use crate::graph_scene::{HitClip, HitGeometry};
11use crate::primitive_emit::resolve_clip;
12
13pub trait HitGraphSink {
14    fn push_hit(
15        &mut self,
16        node_id: NodeId,
17        geometry: HitGeometry,
18        shape: Option<RoundedCornerShape>,
19        click_actions: &[Rc<dyn Fn(Point)>],
20        pointer_inputs: &[Rc<dyn Fn(PointerEvent)>],
21    );
22}
23
24pub fn collect_hits_from_graph<S: HitGraphSink>(
25    layer: &LayerNode,
26    parent_transform: ProjectiveTransform,
27    sink: &mut S,
28    parent_hit_clip: Option<Rect>,
29) {
30    if !layer.has_hit_targets {
31        return;
32    }
33    let mut hit_clips = Vec::new();
34    collect_hits_from_graph_inner(
35        layer,
36        parent_transform,
37        sink,
38        parent_hit_clip,
39        &mut hit_clips,
40    );
41}
42
43fn collect_hits_from_graph_inner<S: HitGraphSink>(
44    layer: &LayerNode,
45    parent_transform: ProjectiveTransform,
46    sink: &mut S,
47    parent_hit_clip: Option<Rect>,
48    hit_clips: &mut Vec<HitClip>,
49) {
50    if !layer.has_hit_targets {
51        return;
52    }
53    let transform = layer.transform_to_parent.then(parent_transform);
54    let transformed_quad = transform.map_rect(layer.local_bounds);
55    let transformed_rect = quad_bounds(transformed_quad);
56
57    if transformed_rect.width <= 0.0 || transformed_rect.height <= 0.0 {
58        return;
59    }
60
61    let Some(world_to_local) = transform.inverse() else {
62        return;
63    };
64
65    let mut hit_clip_bounds = parent_hit_clip;
66    let mut pushed_clip = false;
67    if let Some(local_clip) = layer.clip_rect() {
68        let clip_quad = transform.map_rect(local_clip);
69        let clip_bounds = quad_bounds(clip_quad);
70        let Some(resolved_clip_bounds) = resolve_clip(parent_hit_clip, Some(clip_bounds)) else {
71            return;
72        };
73        hit_clip_bounds = Some(resolved_clip_bounds);
74        hit_clips.push(HitClip {
75            quad: clip_quad,
76            bounds: clip_bounds,
77        });
78        pushed_clip = true;
79    }
80
81    if let (Some(node_id), Some(hit)) = (layer.node_id, &layer.hit_test) {
82        sink.push_hit(
83            node_id,
84            HitGeometry {
85                rect: transformed_rect,
86                quad: transformed_quad,
87                local_bounds: layer.local_bounds,
88                world_to_local,
89                hit_clip_bounds,
90                hit_clips: hit_clips.to_vec(),
91            },
92            hit.shape,
93            &hit.click_actions,
94            &hit.pointer_inputs,
95        );
96    }
97
98    for child in &layer.children {
99        if let RenderNode::Layer(child_layer) = child {
100            collect_hits_from_graph_inner(child_layer, transform, sink, hit_clip_bounds, hit_clips);
101        }
102    }
103
104    if pushed_clip {
105        let _ = hit_clips.pop();
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use crate::graph::{CachePolicy, HitTestNode, IsolationReasons};
113    use crate::raster_cache::LayerRasterCacheHashes;
114
115    #[derive(Default)]
116    struct TestSink {
117        hits: Vec<(NodeId, Rect, [[f32; 2]; 4], Option<Rect>, usize)>,
118    }
119
120    impl HitGraphSink for TestSink {
121        fn push_hit(
122            &mut self,
123            node_id: NodeId,
124            geometry: HitGeometry,
125            _shape: Option<RoundedCornerShape>,
126            _click_actions: &[Rc<dyn Fn(Point)>],
127            _pointer_inputs: &[Rc<dyn Fn(PointerEvent)>],
128        ) {
129            self.hits.push((
130                node_id,
131                geometry.rect,
132                geometry.quad,
133                geometry.hit_clip_bounds,
134                geometry.hit_clips.len(),
135            ));
136        }
137    }
138
139    fn test_layer(node_id: NodeId, transform_to_parent: ProjectiveTransform) -> LayerNode {
140        LayerNode {
141            node_id: Some(node_id),
142            local_bounds: Rect {
143                x: 0.0,
144                y: 0.0,
145                width: 30.0,
146                height: 18.0,
147            },
148            transform_to_parent,
149            motion_context_animated: false,
150            translated_content_context: false,
151            graphics_layer: cranpose_ui_graphics::GraphicsLayer::default(),
152            clip_to_bounds: true,
153            shadow_clip: None,
154            hit_test: Some(HitTestNode {
155                shape: None,
156                click_actions: vec![Rc::new(|_point| {})],
157                pointer_inputs: vec![],
158                clip: None,
159            }),
160            has_hit_targets: true,
161            isolation: IsolationReasons::default(),
162            cache_policy: CachePolicy::None,
163            cache_hashes: LayerRasterCacheHashes::default(),
164            cache_hashes_valid: false,
165            children: vec![],
166        }
167    }
168
169    #[test]
170    fn collect_hits_uses_graph_transform_to_parent() {
171        let layer = test_layer(7, ProjectiveTransform::translation(12.0, 9.0));
172        let mut sink = TestSink::default();
173
174        collect_hits_from_graph(&layer, ProjectiveTransform::identity(), &mut sink, None);
175
176        assert_eq!(sink.hits.len(), 1);
177        let (node_id, rect, quad, clip, clip_count) = sink.hits[0];
178        assert_eq!(node_id, 7);
179        assert_eq!(
180            rect,
181            Rect {
182                x: 12.0,
183                y: 9.0,
184                width: 30.0,
185                height: 18.0,
186            }
187        );
188        assert_eq!(quad, [[12.0, 9.0], [42.0, 9.0], [12.0, 27.0], [42.0, 27.0]]);
189        assert_eq!(clip, Some(rect));
190        assert_eq!(clip_count, 1);
191    }
192
193    #[test]
194    fn collect_hits_composes_nested_graph_transforms() {
195        let child = test_layer(9, ProjectiveTransform::translation(4.0, 3.0));
196        let mut parent = test_layer(7, ProjectiveTransform::translation(10.0, 6.0));
197        parent.children.push(RenderNode::Layer(Box::new(child)));
198        let mut sink = TestSink::default();
199
200        collect_hits_from_graph(&parent, ProjectiveTransform::identity(), &mut sink, None);
201
202        assert_eq!(sink.hits.len(), 2);
203        let (_, child_rect, child_quad, child_clip, child_clip_count) = sink.hits[1];
204        assert_eq!(
205            child_rect,
206            Rect {
207                x: 14.0,
208                y: 9.0,
209                width: 30.0,
210                height: 18.0,
211            }
212        );
213        assert_eq!(
214            child_quad,
215            [[14.0, 9.0], [44.0, 9.0], [14.0, 27.0], [44.0, 27.0]]
216        );
217        assert_eq!(
218            child_clip,
219            Some(Rect {
220                x: 14.0,
221                y: 9.0,
222                width: 26.0,
223                height: 15.0,
224            })
225        );
226        assert_eq!(child_clip_count, 2);
227    }
228
229    #[test]
230    fn collect_hits_retains_transformed_clip_chain() {
231        let mut parent = test_layer(1, ProjectiveTransform::translation(20.0, 10.0));
232        let mut child = test_layer(
233            2,
234            ProjectiveTransform::from_rect_to_quad(
235                Rect {
236                    x: 0.0,
237                    y: 0.0,
238                    width: 30.0,
239                    height: 18.0,
240                },
241                [[0.0, 0.0], [30.0, 0.0], [4.0, 18.0], [34.0, 18.0]],
242            ),
243        );
244        child.clip_to_bounds = true;
245        parent.children.push(RenderNode::Layer(Box::new(child)));
246
247        let mut sink = TestSink::default();
248        collect_hits_from_graph(&parent, ProjectiveTransform::identity(), &mut sink, None);
249
250        let (_, _, _, _, child_clip_count) = sink.hits[1];
251        assert_eq!(child_clip_count, 2);
252    }
253}