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