Skip to main content

cranpose_render_common/
hit_graph.rs

1use cranpose_core::NodeId;
2use cranpose_ui_graphics::Rect;
3use smallvec::SmallVec;
4
5use crate::{
6    graph::{HitTestNode, LayerNode, ProjectiveTransform, RenderNode, quad_bounds},
7    graph_scene::{ClickAction, HitClip, HitGeometry, HitTargetSpec, Scene},
8    primitive_emit::resolve_clip,
9};
10
11pub trait HitGraphSink {
12    fn push_hit(
13        &mut self,
14        node_id: NodeId,
15        capture_path: &[NodeId],
16        geometry: HitGeometry<'_>,
17        hit: &HitTestNode,
18    );
19}
20
21impl HitGraphSink for Scene {
22    fn push_hit(
23        &mut self,
24        node_id: NodeId,
25        capture_path: &[NodeId],
26        geometry: HitGeometry<'_>,
27        hit: &HitTestNode,
28    ) {
29        Scene::push_hit(
30            self,
31            node_id,
32            capture_path,
33            geometry,
34            HitTargetSpec {
35                shape: hit.shape,
36                click_actions: hit
37                    .click_actions
38                    .iter()
39                    .cloned()
40                    .map(ClickAction::WithPoint),
41                pointer_inputs: &hit.pointer_inputs,
42                pointer_icon: hit.pointer_icon.as_ref(),
43            },
44        );
45    }
46}
47
48pub fn collect_hits_from_graph<S: HitGraphSink>(
49    layer: &LayerNode,
50    parent_transform: ProjectiveTransform,
51    sink: &mut S,
52    parent_hit_clip: Option<Rect>,
53) {
54    if !layer.has_hit_targets {
55        return;
56    }
57    let mut hit_clips = Vec::new();
58    let mut pointer_input_ancestors = Vec::new();
59    let mut capture_path = SmallVec::<[NodeId; 8]>::new();
60    collect_hits_from_graph_inner(
61        layer,
62        parent_transform,
63        sink,
64        parent_hit_clip,
65        &mut hit_clips,
66        &mut pointer_input_ancestors,
67        &mut capture_path,
68    );
69}
70
71fn collect_hits_from_graph_inner<S: HitGraphSink>(
72    layer: &LayerNode,
73    parent_transform: ProjectiveTransform,
74    sink: &mut S,
75    parent_hit_clip: Option<Rect>,
76    hit_clips: &mut Vec<HitClip>,
77    pointer_input_ancestors: &mut Vec<NodeId>,
78    capture_path: &mut SmallVec<[NodeId; 8]>,
79) {
80    if !layer.has_hit_targets {
81        return;
82    }
83    let transform = layer.transform_to_parent.then(parent_transform);
84    let transformed_quad = transform.map_rect(layer.local_bounds);
85    let transformed_rect = quad_bounds(transformed_quad);
86
87    if transformed_rect.width <= 0.0 || transformed_rect.height <= 0.0 {
88        return;
89    }
90
91    let Some(world_to_local) = transform.inverse() else {
92        return;
93    };
94
95    let mut hit_clip_bounds = parent_hit_clip;
96    let mut pushed_clip = false;
97    if let Some(local_clip) = layer.clip_rect() {
98        let clip_quad = transform.map_rect(local_clip);
99        let clip_bounds = quad_bounds(clip_quad);
100        let resolved_clip_bounds = resolve_clip(parent_hit_clip, Some(clip_bounds));
101        if resolved_clip_bounds.is_some_and(|clip| clip.is_empty()) {
102            return;
103        }
104        hit_clip_bounds = resolved_clip_bounds;
105        hit_clips.push(HitClip {
106            quad: clip_quad,
107            bounds: clip_bounds,
108        });
109        pushed_clip = true;
110    }
111
112    if let (Some(node_id), Some(hit)) = (layer.node_id, &layer.hit_test) {
113        capture_path.clear();
114        capture_path.push(node_id);
115        capture_path.extend(pointer_input_ancestors.iter().rev().copied());
116        sink.push_hit(
117            node_id,
118            capture_path,
119            HitGeometry {
120                rect: transformed_rect,
121                quad: transformed_quad,
122                local_bounds: layer.local_bounds,
123                world_to_local,
124                hit_clip_bounds,
125                hit_clips,
126            },
127            hit,
128        );
129    }
130
131    let pointer_input_ancestor = layer
132        .hit_test
133        .as_ref()
134        .filter(|hit| !hit.pointer_inputs.is_empty())
135        .and(layer.node_id);
136    if let Some(node_id) = pointer_input_ancestor {
137        pointer_input_ancestors.push(node_id);
138    }
139
140    for child in &layer.children {
141        if let RenderNode::Layer(child_layer) = child {
142            collect_hits_from_graph_inner(
143                child_layer,
144                transform,
145                sink,
146                hit_clip_bounds,
147                hit_clips,
148                pointer_input_ancestors,
149                capture_path,
150            );
151        }
152    }
153
154    if pointer_input_ancestor.is_some() {
155        let _ = pointer_input_ancestors.pop();
156    }
157
158    if pushed_clip {
159        let _ = hit_clips.pop();
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use std::rc::Rc;
166
167    use super::*;
168    use crate::graph::HitTestNode;
169
170    type RecordedHit = (
171        NodeId,
172        Vec<NodeId>,
173        Rect,
174        [[f32; 2]; 4],
175        Option<Rect>,
176        usize,
177    );
178
179    #[derive(Default)]
180    struct TestSink {
181        hits: Vec<RecordedHit>,
182    }
183
184    impl HitGraphSink for TestSink {
185        fn push_hit(
186            &mut self,
187            node_id: NodeId,
188            capture_path: &[NodeId],
189            geometry: HitGeometry<'_>,
190            _hit: &HitTestNode,
191        ) {
192            self.hits.push((
193                node_id,
194                capture_path.to_vec(),
195                geometry.rect,
196                geometry.quad,
197                geometry.hit_clip_bounds,
198                geometry.hit_clips.len(),
199            ));
200        }
201    }
202
203    fn test_layer(node_id: NodeId, transform_to_parent: ProjectiveTransform) -> LayerNode {
204        LayerNode {
205            node_id: Some(node_id),
206            local_bounds: Rect {
207                x: 0.0,
208                y: 0.0,
209                width: 30.0,
210                height: 18.0,
211            },
212            transform_to_parent,
213            clip_to_bounds: true,
214            hit_test: Some(HitTestNode {
215                shape: None,
216                click_actions: vec![Rc::new(|_point| {})],
217                pointer_inputs: vec![],
218                pointer_icon: None,
219                clip: None,
220            }),
221            has_hit_targets: true,
222            ..Default::default()
223        }
224    }
225
226    #[test]
227    fn collect_hits_uses_graph_transform_to_parent() {
228        let layer = test_layer(7, ProjectiveTransform::translation(12.0, 9.0));
229        let mut sink = TestSink::default();
230
231        collect_hits_from_graph(&layer, ProjectiveTransform::identity(), &mut sink, None);
232
233        assert_eq!(sink.hits.len(), 1);
234        let (node_id, capture_path, rect, quad, clip, clip_count) = &sink.hits[0];
235        assert_eq!(*node_id, 7);
236        assert_eq!(capture_path, &vec![7]);
237        assert_eq!(
238            *rect,
239            Rect {
240                x: 12.0,
241                y: 9.0,
242                width: 30.0,
243                height: 18.0,
244            }
245        );
246        assert_eq!(
247            *quad,
248            [[12.0, 9.0], [42.0, 9.0], [12.0, 27.0], [42.0, 27.0]]
249        );
250        assert_eq!(*clip, Some(*rect));
251        assert_eq!(*clip_count, 1);
252    }
253
254    #[test]
255    fn a_child_clipped_away_by_its_parent_takes_no_hits() {
256        let mut list = test_layer(1, ProjectiveTransform::translation(0.0, 80.0));
257        let scrolled_out = test_layer(2, ProjectiveTransform::translation(4.0, -40.0));
258        list.children
259            .push(RenderNode::Layer(Box::new(scrolled_out)));
260        let mut sink = TestSink::default();
261
262        collect_hits_from_graph(&list, ProjectiveTransform::identity(), &mut sink, None);
263
264        let hit_ids: Vec<NodeId> = sink.hits.iter().map(|hit| hit.0).collect();
265        assert_eq!(
266            hit_ids,
267            vec![1],
268            "a child the list has scrolled past its edge lies outside the list's clip and \
269             takes no hits, however far inside the window it sits"
270        );
271    }
272
273    #[test]
274    fn collect_hits_composes_nested_graph_transforms() {
275        let child = test_layer(9, ProjectiveTransform::translation(4.0, 3.0));
276        let mut parent = test_layer(7, ProjectiveTransform::translation(10.0, 6.0));
277        parent.hit_test.as_mut().expect("hit test").pointer_inputs = vec![Rc::new(|_event| {})];
278        parent.children.push(RenderNode::Layer(Box::new(child)));
279        let mut sink = TestSink::default();
280
281        collect_hits_from_graph(&parent, ProjectiveTransform::identity(), &mut sink, None);
282
283        assert_eq!(sink.hits.len(), 2);
284        let (_, child_capture_path, child_rect, child_quad, child_clip, child_clip_count) =
285            &sink.hits[1];
286        assert_eq!(child_capture_path, &vec![9, 7]);
287        assert_eq!(
288            *child_rect,
289            Rect {
290                x: 14.0,
291                y: 9.0,
292                width: 30.0,
293                height: 18.0,
294            }
295        );
296        assert_eq!(
297            *child_quad,
298            [[14.0, 9.0], [44.0, 9.0], [14.0, 27.0], [44.0, 27.0]]
299        );
300        assert_eq!(
301            *child_clip,
302            Some(Rect {
303                x: 14.0,
304                y: 9.0,
305                width: 26.0,
306                height: 15.0,
307            })
308        );
309        assert_eq!(*child_clip_count, 2);
310    }
311
312    #[test]
313    fn capture_paths_do_not_leak_between_siblings_or_traversals() {
314        let identity = ProjectiveTransform::identity();
315        let mut root = test_layer(12, identity);
316        for node_id in (1..12).rev() {
317            let mut parent = test_layer(node_id, identity);
318            parent.hit_test.as_mut().unwrap().pointer_inputs = vec![Rc::new(|_| {})];
319            parent.children.push(RenderNode::Layer(Box::new(root)));
320            root = parent;
321        }
322        root.children
323            .push(RenderNode::Layer(Box::new(test_layer(13, identity))));
324        let mut sink = TestSink::default();
325        collect_hits_from_graph(&root, identity, &mut sink, None);
326        collect_hits_from_graph(&test_layer(14, identity), identity, &mut sink, None);
327        let mut expected: Vec<Vec<_>> = (1..=12)
328            .map(|node_id| (1..=node_id).rev().collect())
329            .collect();
330        expected.extend([vec![13, 1], vec![14]]);
331        let paths: Vec<_> = sink.hits.into_iter().map(|hit| hit.1).collect();
332        assert_eq!(paths, expected);
333    }
334
335    #[test]
336    fn collect_hits_retains_transformed_clip_chain() {
337        let mut parent = test_layer(1, ProjectiveTransform::translation(20.0, 10.0));
338        let mut child = test_layer(
339            2,
340            ProjectiveTransform::from_rect_to_quad(
341                Rect {
342                    x: 0.0,
343                    y: 0.0,
344                    width: 30.0,
345                    height: 18.0,
346                },
347                [[0.0, 0.0], [30.0, 0.0], [4.0, 18.0], [34.0, 18.0]],
348            ),
349        );
350        child.clip_to_bounds = true;
351        parent.children.push(RenderNode::Layer(Box::new(child)));
352
353        let mut sink = TestSink::default();
354        collect_hits_from_graph(&parent, ProjectiveTransform::identity(), &mut sink, None);
355
356        let (_, _, _, _, _, child_clip_count) = sink.hits[1];
357        assert_eq!(child_clip_count, 2);
358    }
359}