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}