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