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