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};
7use smallvec::SmallVec;
8
9use crate::{
10    graph::{LayerNode, ProjectiveTransform, RenderNode, quad_bounds},
11    graph_scene::{ClickAction, HitClip, HitGeometry, Scene},
12    primitive_emit::resolve_clip,
13};
14
15pub trait HitGraphSink {
16    fn push_hit(
17        &mut self,
18        node_id: NodeId,
19        capture_path: &[NodeId],
20        geometry: HitGeometry<'_>,
21        shape: Option<RoundedCornerShape>,
22        click_actions: &[Rc<dyn Fn(Point)>],
23        pointer_inputs: &[Rc<dyn Fn(PointerEvent)>],
24    );
25}
26
27impl HitGraphSink for Scene {
28    fn push_hit(
29        &mut self,
30        node_id: NodeId,
31        capture_path: &[NodeId],
32        geometry: HitGeometry<'_>,
33        shape: Option<RoundedCornerShape>,
34        click_actions: &[Rc<dyn Fn(Point)>],
35        pointer_inputs: &[Rc<dyn Fn(PointerEvent)>],
36    ) {
37        Scene::push_hit(
38            self,
39            node_id,
40            capture_path,
41            geometry,
42            shape,
43            click_actions.iter().cloned().map(ClickAction::WithPoint),
44            pointer_inputs,
45        );
46    }
47}
48
49pub fn collect_hits_from_graph<S: HitGraphSink>(
50    layer: &LayerNode,
51    parent_transform: ProjectiveTransform,
52    sink: &mut S,
53    parent_hit_clip: Option<Rect>,
54) {
55    if !layer.has_hit_targets {
56        return;
57    }
58    let mut hit_clips = Vec::new();
59    let mut pointer_input_ancestors = Vec::new();
60    let mut capture_path = SmallVec::<[NodeId; 8]>::new();
61    collect_hits_from_graph_inner(
62        layer,
63        parent_transform,
64        sink,
65        parent_hit_clip,
66        &mut hit_clips,
67        &mut pointer_input_ancestors,
68        &mut capture_path,
69    );
70}
71
72fn collect_hits_from_graph_inner<S: HitGraphSink>(
73    layer: &LayerNode,
74    parent_transform: ProjectiveTransform,
75    sink: &mut S,
76    parent_hit_clip: Option<Rect>,
77    hit_clips: &mut Vec<HitClip>,
78    pointer_input_ancestors: &mut Vec<NodeId>,
79    capture_path: &mut SmallVec<[NodeId; 8]>,
80) {
81    if !layer.has_hit_targets {
82        return;
83    }
84    let transform = layer.transform_to_parent.then(parent_transform);
85    let transformed_quad = transform.map_rect(layer.local_bounds);
86    let transformed_rect = quad_bounds(transformed_quad);
87
88    if transformed_rect.width <= 0.0 || transformed_rect.height <= 0.0 {
89        return;
90    }
91
92    let Some(world_to_local) = transform.inverse() else {
93        return;
94    };
95
96    let mut hit_clip_bounds = parent_hit_clip;
97    let mut pushed_clip = false;
98    if let Some(local_clip) = layer.clip_rect() {
99        let clip_quad = transform.map_rect(local_clip);
100        let clip_bounds = quad_bounds(clip_quad);
101        let Some(resolved_clip_bounds) = resolve_clip(parent_hit_clip, Some(clip_bounds)) else {
102            return;
103        };
104        hit_clip_bounds = Some(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.shape,
128            &hit.click_actions,
129            &hit.pointer_inputs,
130        );
131    }
132
133    let pointer_input_ancestor = layer
134        .hit_test
135        .as_ref()
136        .filter(|hit| !hit.pointer_inputs.is_empty())
137        .and(layer.node_id);
138    if let Some(node_id) = pointer_input_ancestor {
139        pointer_input_ancestors.push(node_id);
140    }
141
142    for child in &layer.children {
143        if let RenderNode::Layer(child_layer) = child {
144            collect_hits_from_graph_inner(
145                child_layer,
146                transform,
147                sink,
148                hit_clip_bounds,
149                hit_clips,
150                pointer_input_ancestors,
151                capture_path,
152            );
153        }
154    }
155
156    if pointer_input_ancestor.is_some() {
157        let _ = pointer_input_ancestors.pop();
158    }
159
160    if pushed_clip {
161        let _ = hit_clips.pop();
162    }
163}
164
165#[cfg(test)]
166mod tests {
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            _shape: Option<RoundedCornerShape>,
191            _click_actions: &[Rc<dyn Fn(Point)>],
192            _pointer_inputs: &[Rc<dyn Fn(PointerEvent)>],
193        ) {
194            self.hits.push((
195                node_id,
196                capture_path.to_vec(),
197                geometry.rect,
198                geometry.quad,
199                geometry.hit_clip_bounds,
200                geometry.hit_clips.len(),
201            ));
202        }
203    }
204
205    fn test_layer(node_id: NodeId, transform_to_parent: ProjectiveTransform) -> LayerNode {
206        LayerNode {
207            node_id: Some(node_id),
208            local_bounds: Rect {
209                x: 0.0,
210                y: 0.0,
211                width: 30.0,
212                height: 18.0,
213            },
214            transform_to_parent,
215            clip_to_bounds: true,
216            hit_test: Some(HitTestNode {
217                shape: None,
218                click_actions: vec![Rc::new(|_point| {})],
219                pointer_inputs: vec![],
220                clip: None,
221            }),
222            has_hit_targets: true,
223            ..Default::default()
224        }
225    }
226
227    #[test]
228    fn collect_hits_uses_graph_transform_to_parent() {
229        let layer = test_layer(7, ProjectiveTransform::translation(12.0, 9.0));
230        let mut sink = TestSink::default();
231
232        collect_hits_from_graph(&layer, ProjectiveTransform::identity(), &mut sink, None);
233
234        assert_eq!(sink.hits.len(), 1);
235        let (node_id, capture_path, rect, quad, clip, clip_count) = &sink.hits[0];
236        assert_eq!(*node_id, 7);
237        assert_eq!(capture_path, &vec![7]);
238        assert_eq!(
239            *rect,
240            Rect {
241                x: 12.0,
242                y: 9.0,
243                width: 30.0,
244                height: 18.0,
245            }
246        );
247        assert_eq!(
248            *quad,
249            [[12.0, 9.0], [42.0, 9.0], [12.0, 27.0], [42.0, 27.0]]
250        );
251        assert_eq!(*clip, Some(*rect));
252        assert_eq!(*clip_count, 1);
253    }
254
255    #[test]
256    fn collect_hits_composes_nested_graph_transforms() {
257        let child = test_layer(9, ProjectiveTransform::translation(4.0, 3.0));
258        let mut parent = test_layer(7, ProjectiveTransform::translation(10.0, 6.0));
259        parent.hit_test.as_mut().expect("hit test").pointer_inputs = vec![Rc::new(|_event| {})];
260        parent.children.push(RenderNode::Layer(Box::new(child)));
261        let mut sink = TestSink::default();
262
263        collect_hits_from_graph(&parent, ProjectiveTransform::identity(), &mut sink, None);
264
265        assert_eq!(sink.hits.len(), 2);
266        let (_, child_capture_path, child_rect, child_quad, child_clip, child_clip_count) =
267            &sink.hits[1];
268        assert_eq!(child_capture_path, &vec![9, 7]);
269        assert_eq!(
270            *child_rect,
271            Rect {
272                x: 14.0,
273                y: 9.0,
274                width: 30.0,
275                height: 18.0,
276            }
277        );
278        assert_eq!(
279            *child_quad,
280            [[14.0, 9.0], [44.0, 9.0], [14.0, 27.0], [44.0, 27.0]]
281        );
282        assert_eq!(
283            *child_clip,
284            Some(Rect {
285                x: 14.0,
286                y: 9.0,
287                width: 26.0,
288                height: 15.0,
289            })
290        );
291        assert_eq!(*child_clip_count, 2);
292    }
293
294    #[test]
295    fn capture_paths_do_not_leak_between_siblings_or_traversals() {
296        let identity = ProjectiveTransform::identity();
297        let mut root = test_layer(12, identity);
298        for node_id in (1..12).rev() {
299            let mut parent = test_layer(node_id, identity);
300            parent.hit_test.as_mut().unwrap().pointer_inputs = vec![Rc::new(|_| {})];
301            parent.children.push(RenderNode::Layer(Box::new(root)));
302            root = parent;
303        }
304        root.children
305            .push(RenderNode::Layer(Box::new(test_layer(13, identity))));
306        let mut sink = TestSink::default();
307        collect_hits_from_graph(&root, identity, &mut sink, None);
308        collect_hits_from_graph(&test_layer(14, identity), identity, &mut sink, None);
309        let mut expected: Vec<Vec<_>> = (1..=12)
310            .map(|node_id| (1..=node_id).rev().collect())
311            .collect();
312        expected.extend([vec![13, 1], vec![14]]);
313        let paths: Vec<_> = sink.hits.into_iter().map(|hit| hit.1).collect();
314        assert_eq!(paths, expected);
315    }
316
317    #[test]
318    fn collect_hits_retains_transformed_clip_chain() {
319        let mut parent = test_layer(1, ProjectiveTransform::translation(20.0, 10.0));
320        let mut child = test_layer(
321            2,
322            ProjectiveTransform::from_rect_to_quad(
323                Rect {
324                    x: 0.0,
325                    y: 0.0,
326                    width: 30.0,
327                    height: 18.0,
328                },
329                [[0.0, 0.0], [30.0, 0.0], [4.0, 18.0], [34.0, 18.0]],
330            ),
331        );
332        child.clip_to_bounds = true;
333        parent.children.push(RenderNode::Layer(Box::new(child)));
334
335        let mut sink = TestSink::default();
336        collect_hits_from_graph(&parent, ProjectiveTransform::identity(), &mut sink, None);
337
338        let (_, _, _, _, _, child_clip_count) = sink.hits[1];
339        assert_eq!(child_clip_count, 2);
340    }
341}