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}