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::{
9    graph::{LayerNode, ProjectiveTransform, RenderNode, quad_bounds},
10    graph_scene::{ClickAction, HitClip, HitGeometry, Scene},
11    primitive_emit::resolve_clip,
12};
13
14pub trait HitGraphSink {
15    fn push_hit(
16        &mut self,
17        node_id: NodeId,
18        capture_path: &[NodeId],
19        geometry: HitGeometry,
20        shape: Option<RoundedCornerShape>,
21        click_actions: &[Rc<dyn Fn(Point)>],
22        pointer_inputs: &[Rc<dyn Fn(PointerEvent)>],
23    );
24}
25
26impl HitGraphSink for Scene {
27    fn push_hit(
28        &mut self,
29        node_id: NodeId,
30        capture_path: &[NodeId],
31        geometry: HitGeometry,
32        shape: Option<RoundedCornerShape>,
33        click_actions: &[Rc<dyn Fn(Point)>],
34        pointer_inputs: &[Rc<dyn Fn(PointerEvent)>],
35    ) {
36        Scene::push_hit(
37            self,
38            node_id,
39            capture_path.to_vec(),
40            geometry,
41            shape,
42            click_actions
43                .iter()
44                .cloned()
45                .map(ClickAction::WithPoint)
46                .collect(),
47            pointer_inputs.to_vec(),
48        );
49    }
50}
51
52pub fn collect_hits_from_graph<S: HitGraphSink>(
53    layer: &LayerNode,
54    parent_transform: ProjectiveTransform,
55    sink: &mut S,
56    parent_hit_clip: Option<Rect>,
57) {
58    if !layer.has_hit_targets {
59        return;
60    }
61    let mut hit_clips = Vec::new();
62    let mut pointer_input_ancestors = Vec::new();
63    collect_hits_from_graph_inner(
64        layer,
65        parent_transform,
66        sink,
67        parent_hit_clip,
68        &mut hit_clips,
69        &mut pointer_input_ancestors,
70    );
71}
72
73fn collect_hits_from_graph_inner<S: HitGraphSink>(
74    layer: &LayerNode,
75    parent_transform: ProjectiveTransform,
76    sink: &mut S,
77    parent_hit_clip: Option<Rect>,
78    hit_clips: &mut Vec<HitClip>,
79    pointer_input_ancestors: &mut Vec<NodeId>,
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        let mut capture_path = Vec::with_capacity(1 + pointer_input_ancestors.len());
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: hit_clips.to_vec(),
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            );
152        }
153    }
154
155    if pointer_input_ancestor.is_some() {
156        let _ = pointer_input_ancestors.pop();
157    }
158
159    if pushed_clip {
160        let _ = hit_clips.pop();
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167    use crate::graph::HitTestNode;
168
169    type RecordedHit = (
170        NodeId,
171        Vec<NodeId>,
172        Rect,
173        [[f32; 2]; 4],
174        Option<Rect>,
175        usize,
176    );
177
178    #[derive(Default)]
179    struct TestSink {
180        hits: Vec<RecordedHit>,
181    }
182
183    impl HitGraphSink for TestSink {
184        fn push_hit(
185            &mut self,
186            node_id: NodeId,
187            capture_path: &[NodeId],
188            geometry: HitGeometry,
189            _shape: Option<RoundedCornerShape>,
190            _click_actions: &[Rc<dyn Fn(Point)>],
191            _pointer_inputs: &[Rc<dyn Fn(PointerEvent)>],
192        ) {
193            self.hits.push((
194                node_id,
195                capture_path.to_vec(),
196                geometry.rect,
197                geometry.quad,
198                geometry.hit_clip_bounds,
199                geometry.hit_clips.len(),
200            ));
201        }
202    }
203
204    fn test_layer(node_id: NodeId, transform_to_parent: ProjectiveTransform) -> LayerNode {
205        LayerNode {
206            node_id: Some(node_id),
207            local_bounds: Rect {
208                x: 0.0,
209                y: 0.0,
210                width: 30.0,
211                height: 18.0,
212            },
213            transform_to_parent,
214            clip_to_bounds: true,
215            hit_test: Some(HitTestNode {
216                shape: None,
217                click_actions: vec![Rc::new(|_point| {})],
218                pointer_inputs: vec![],
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 collect_hits_composes_nested_graph_transforms() {
256        let child = test_layer(9, ProjectiveTransform::translation(4.0, 3.0));
257        let mut parent = test_layer(7, ProjectiveTransform::translation(10.0, 6.0));
258        parent.hit_test.as_mut().expect("hit test").pointer_inputs = vec![Rc::new(|_event| {})];
259        parent.children.push(RenderNode::Layer(Box::new(child)));
260        let mut sink = TestSink::default();
261
262        collect_hits_from_graph(&parent, ProjectiveTransform::identity(), &mut sink, None);
263
264        assert_eq!(sink.hits.len(), 2);
265        let (_, child_capture_path, child_rect, child_quad, child_clip, child_clip_count) =
266            &sink.hits[1];
267        assert_eq!(child_capture_path, &vec![9, 7]);
268        assert_eq!(
269            *child_rect,
270            Rect {
271                x: 14.0,
272                y: 9.0,
273                width: 30.0,
274                height: 18.0,
275            }
276        );
277        assert_eq!(
278            *child_quad,
279            [[14.0, 9.0], [44.0, 9.0], [14.0, 27.0], [44.0, 27.0]]
280        );
281        assert_eq!(
282            *child_clip,
283            Some(Rect {
284                x: 14.0,
285                y: 9.0,
286                width: 26.0,
287                height: 15.0,
288            })
289        );
290        assert_eq!(*child_clip_count, 2);
291    }
292
293    #[test]
294    fn collect_hits_retains_transformed_clip_chain() {
295        let mut parent = test_layer(1, ProjectiveTransform::translation(20.0, 10.0));
296        let mut child = test_layer(
297            2,
298            ProjectiveTransform::from_rect_to_quad(
299                Rect {
300                    x: 0.0,
301                    y: 0.0,
302                    width: 30.0,
303                    height: 18.0,
304                },
305                [[0.0, 0.0], [30.0, 0.0], [4.0, 18.0], [34.0, 18.0]],
306            ),
307        );
308        child.clip_to_bounds = true;
309        parent.children.push(RenderNode::Layer(Box::new(child)));
310
311        let mut sink = TestSink::default();
312        collect_hits_from_graph(&parent, ProjectiveTransform::identity(), &mut sink, None);
313
314        let (_, _, _, _, _, child_clip_count) = sink.hits[1];
315        assert_eq!(child_clip_count, 2);
316    }
317}