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}