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 resolved_clip_bounds = resolve_clip(parent_hit_clip, Some(clip_bounds));
102 if resolved_clip_bounds.is_some_and(|clip| clip.is_empty()) {
103 return;
104 }
105 hit_clip_bounds = resolved_clip_bounds;
106 hit_clips.push(HitClip {
107 quad: clip_quad,
108 bounds: clip_bounds,
109 });
110 pushed_clip = true;
111 }
112
113 if let (Some(node_id), Some(hit)) = (layer.node_id, &layer.hit_test) {
114 capture_path.clear();
115 capture_path.push(node_id);
116 capture_path.extend(pointer_input_ancestors.iter().rev().copied());
117 sink.push_hit(
118 node_id,
119 capture_path,
120 HitGeometry {
121 rect: transformed_rect,
122 quad: transformed_quad,
123 local_bounds: layer.local_bounds,
124 world_to_local,
125 hit_clip_bounds,
126 hit_clips,
127 },
128 hit.shape,
129 &hit.click_actions,
130 &hit.pointer_inputs,
131 );
132 }
133
134 let pointer_input_ancestor = layer
135 .hit_test
136 .as_ref()
137 .filter(|hit| !hit.pointer_inputs.is_empty())
138 .and(layer.node_id);
139 if let Some(node_id) = pointer_input_ancestor {
140 pointer_input_ancestors.push(node_id);
141 }
142
143 for child in &layer.children {
144 if let RenderNode::Layer(child_layer) = child {
145 collect_hits_from_graph_inner(
146 child_layer,
147 transform,
148 sink,
149 hit_clip_bounds,
150 hit_clips,
151 pointer_input_ancestors,
152 capture_path,
153 );
154 }
155 }
156
157 if pointer_input_ancestor.is_some() {
158 let _ = pointer_input_ancestors.pop();
159 }
160
161 if pushed_clip {
162 let _ = hit_clips.pop();
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169 use crate::graph::HitTestNode;
170
171 type RecordedHit = (
172 NodeId,
173 Vec<NodeId>,
174 Rect,
175 [[f32; 2]; 4],
176 Option<Rect>,
177 usize,
178 );
179
180 #[derive(Default)]
181 struct TestSink {
182 hits: Vec<RecordedHit>,
183 }
184
185 impl HitGraphSink for TestSink {
186 fn push_hit(
187 &mut self,
188 node_id: NodeId,
189 capture_path: &[NodeId],
190 geometry: HitGeometry<'_>,
191 _shape: Option<RoundedCornerShape>,
192 _click_actions: &[Rc<dyn Fn(Point)>],
193 _pointer_inputs: &[Rc<dyn Fn(PointerEvent)>],
194 ) {
195 self.hits.push((
196 node_id,
197 capture_path.to_vec(),
198 geometry.rect,
199 geometry.quad,
200 geometry.hit_clip_bounds,
201 geometry.hit_clips.len(),
202 ));
203 }
204 }
205
206 fn test_layer(node_id: NodeId, transform_to_parent: ProjectiveTransform) -> LayerNode {
207 LayerNode {
208 node_id: Some(node_id),
209 local_bounds: Rect {
210 x: 0.0,
211 y: 0.0,
212 width: 30.0,
213 height: 18.0,
214 },
215 transform_to_parent,
216 clip_to_bounds: true,
217 hit_test: Some(HitTestNode {
218 shape: None,
219 click_actions: vec![Rc::new(|_point| {})],
220 pointer_inputs: vec![],
221 clip: None,
222 }),
223 has_hit_targets: true,
224 ..Default::default()
225 }
226 }
227
228 #[test]
229 fn collect_hits_uses_graph_transform_to_parent() {
230 let layer = test_layer(7, ProjectiveTransform::translation(12.0, 9.0));
231 let mut sink = TestSink::default();
232
233 collect_hits_from_graph(&layer, ProjectiveTransform::identity(), &mut sink, None);
234
235 assert_eq!(sink.hits.len(), 1);
236 let (node_id, capture_path, rect, quad, clip, clip_count) = &sink.hits[0];
237 assert_eq!(*node_id, 7);
238 assert_eq!(capture_path, &vec![7]);
239 assert_eq!(
240 *rect,
241 Rect {
242 x: 12.0,
243 y: 9.0,
244 width: 30.0,
245 height: 18.0,
246 }
247 );
248 assert_eq!(
249 *quad,
250 [[12.0, 9.0], [42.0, 9.0], [12.0, 27.0], [42.0, 27.0]]
251 );
252 assert_eq!(*clip, Some(*rect));
253 assert_eq!(*clip_count, 1);
254 }
255
256 #[test]
257 fn a_child_clipped_away_by_its_parent_takes_no_hits() {
258 let mut list = test_layer(1, ProjectiveTransform::translation(0.0, 80.0));
259 let scrolled_out = test_layer(2, ProjectiveTransform::translation(4.0, -40.0));
260 list.children
261 .push(RenderNode::Layer(Box::new(scrolled_out)));
262 let mut sink = TestSink::default();
263
264 collect_hits_from_graph(&list, ProjectiveTransform::identity(), &mut sink, None);
265
266 let hit_ids: Vec<NodeId> = sink.hits.iter().map(|hit| hit.0).collect();
267 assert_eq!(
268 hit_ids,
269 vec![1],
270 "a child the list has scrolled past its edge lies outside the list's clip and \
271 takes no hits, however far inside the window it sits"
272 );
273 }
274
275 #[test]
276 fn collect_hits_composes_nested_graph_transforms() {
277 let child = test_layer(9, ProjectiveTransform::translation(4.0, 3.0));
278 let mut parent = test_layer(7, ProjectiveTransform::translation(10.0, 6.0));
279 parent.hit_test.as_mut().expect("hit test").pointer_inputs = vec![Rc::new(|_event| {})];
280 parent.children.push(RenderNode::Layer(Box::new(child)));
281 let mut sink = TestSink::default();
282
283 collect_hits_from_graph(&parent, ProjectiveTransform::identity(), &mut sink, None);
284
285 assert_eq!(sink.hits.len(), 2);
286 let (_, child_capture_path, child_rect, child_quad, child_clip, child_clip_count) =
287 &sink.hits[1];
288 assert_eq!(child_capture_path, &vec![9, 7]);
289 assert_eq!(
290 *child_rect,
291 Rect {
292 x: 14.0,
293 y: 9.0,
294 width: 30.0,
295 height: 18.0,
296 }
297 );
298 assert_eq!(
299 *child_quad,
300 [[14.0, 9.0], [44.0, 9.0], [14.0, 27.0], [44.0, 27.0]]
301 );
302 assert_eq!(
303 *child_clip,
304 Some(Rect {
305 x: 14.0,
306 y: 9.0,
307 width: 26.0,
308 height: 15.0,
309 })
310 );
311 assert_eq!(*child_clip_count, 2);
312 }
313
314 #[test]
315 fn capture_paths_do_not_leak_between_siblings_or_traversals() {
316 let identity = ProjectiveTransform::identity();
317 let mut root = test_layer(12, identity);
318 for node_id in (1..12).rev() {
319 let mut parent = test_layer(node_id, identity);
320 parent.hit_test.as_mut().unwrap().pointer_inputs = vec![Rc::new(|_| {})];
321 parent.children.push(RenderNode::Layer(Box::new(root)));
322 root = parent;
323 }
324 root.children
325 .push(RenderNode::Layer(Box::new(test_layer(13, identity))));
326 let mut sink = TestSink::default();
327 collect_hits_from_graph(&root, identity, &mut sink, None);
328 collect_hits_from_graph(&test_layer(14, identity), identity, &mut sink, None);
329 let mut expected: Vec<Vec<_>> = (1..=12)
330 .map(|node_id| (1..=node_id).rev().collect())
331 .collect();
332 expected.extend([vec![13, 1], vec![14]]);
333 let paths: Vec<_> = sink.hits.into_iter().map(|hit| hit.1).collect();
334 assert_eq!(paths, expected);
335 }
336
337 #[test]
338 fn collect_hits_retains_transformed_clip_chain() {
339 let mut parent = test_layer(1, ProjectiveTransform::translation(20.0, 10.0));
340 let mut child = test_layer(
341 2,
342 ProjectiveTransform::from_rect_to_quad(
343 Rect {
344 x: 0.0,
345 y: 0.0,
346 width: 30.0,
347 height: 18.0,
348 },
349 [[0.0, 0.0], [30.0, 0.0], [4.0, 18.0], [34.0, 18.0]],
350 ),
351 );
352 child.clip_to_bounds = true;
353 parent.children.push(RenderNode::Layer(Box::new(child)));
354
355 let mut sink = TestSink::default();
356 collect_hits_from_graph(&parent, ProjectiveTransform::identity(), &mut sink, None);
357
358 let (_, _, _, _, _, child_clip_count) = sink.hits[1];
359 assert_eq!(child_clip_count, 2);
360 }
361}