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::graph::quad_bounds;
9use crate::graph::{LayerNode, ProjectiveTransform, RenderNode};
10use crate::graph_scene::{HitClip, HitGeometry};
11use crate::primitive_emit::resolve_clip;
12
13pub trait HitGraphSink {
14 fn push_hit(
15 &mut self,
16 node_id: NodeId,
17 capture_path: &[NodeId],
18 geometry: HitGeometry,
19 shape: Option<RoundedCornerShape>,
20 click_actions: &[Rc<dyn Fn(Point)>],
21 pointer_inputs: &[Rc<dyn Fn(PointerEvent)>],
22 );
23}
24
25pub fn collect_hits_from_graph<S: HitGraphSink>(
26 layer: &LayerNode,
27 parent_transform: ProjectiveTransform,
28 sink: &mut S,
29 parent_hit_clip: Option<Rect>,
30) {
31 if !layer.has_hit_targets {
32 return;
33 }
34 let mut hit_clips = Vec::new();
35 let mut pointer_input_ancestors = Vec::new();
36 collect_hits_from_graph_inner(
37 layer,
38 parent_transform,
39 sink,
40 parent_hit_clip,
41 &mut hit_clips,
42 &mut pointer_input_ancestors,
43 );
44}
45
46fn collect_hits_from_graph_inner<S: HitGraphSink>(
47 layer: &LayerNode,
48 parent_transform: ProjectiveTransform,
49 sink: &mut S,
50 parent_hit_clip: Option<Rect>,
51 hit_clips: &mut Vec<HitClip>,
52 pointer_input_ancestors: &mut Vec<NodeId>,
53) {
54 if !layer.has_hit_targets {
55 return;
56 }
57 let transform = layer.transform_to_parent.then(parent_transform);
58 let transformed_quad = transform.map_rect(layer.local_bounds);
59 let transformed_rect = quad_bounds(transformed_quad);
60
61 if transformed_rect.width <= 0.0 || transformed_rect.height <= 0.0 {
62 return;
63 }
64
65 let Some(world_to_local) = transform.inverse() else {
66 return;
67 };
68
69 let mut hit_clip_bounds = parent_hit_clip;
70 let mut pushed_clip = false;
71 if let Some(local_clip) = layer.clip_rect() {
72 let clip_quad = transform.map_rect(local_clip);
73 let clip_bounds = quad_bounds(clip_quad);
74 let Some(resolved_clip_bounds) = resolve_clip(parent_hit_clip, Some(clip_bounds)) else {
75 return;
76 };
77 hit_clip_bounds = Some(resolved_clip_bounds);
78 hit_clips.push(HitClip {
79 quad: clip_quad,
80 bounds: clip_bounds,
81 });
82 pushed_clip = true;
83 }
84
85 if let (Some(node_id), Some(hit)) = (layer.node_id, &layer.hit_test) {
86 let mut capture_path = Vec::with_capacity(1 + pointer_input_ancestors.len());
87 capture_path.push(node_id);
88 capture_path.extend(pointer_input_ancestors.iter().rev().copied());
89 sink.push_hit(
90 node_id,
91 &capture_path,
92 HitGeometry {
93 rect: transformed_rect,
94 quad: transformed_quad,
95 local_bounds: layer.local_bounds,
96 world_to_local,
97 hit_clip_bounds,
98 hit_clips: hit_clips.to_vec(),
99 },
100 hit.shape,
101 &hit.click_actions,
102 &hit.pointer_inputs,
103 );
104 }
105
106 let pointer_input_ancestor = layer
107 .hit_test
108 .as_ref()
109 .filter(|hit| !hit.pointer_inputs.is_empty())
110 .and(layer.node_id);
111 if let Some(node_id) = pointer_input_ancestor {
112 pointer_input_ancestors.push(node_id);
113 }
114
115 for child in &layer.children {
116 if let RenderNode::Layer(child_layer) = child {
117 collect_hits_from_graph_inner(
118 child_layer,
119 transform,
120 sink,
121 hit_clip_bounds,
122 hit_clips,
123 pointer_input_ancestors,
124 );
125 }
126 }
127
128 if pointer_input_ancestor.is_some() {
129 let _ = pointer_input_ancestors.pop();
130 }
131
132 if pushed_clip {
133 let _ = hit_clips.pop();
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140 use crate::graph::{CachePolicy, HitTestNode, IsolationReasons};
141 use crate::raster_cache::LayerRasterCacheHashes;
142
143 type RecordedHit = (
144 NodeId,
145 Vec<NodeId>,
146 Rect,
147 [[f32; 2]; 4],
148 Option<Rect>,
149 usize,
150 );
151
152 #[derive(Default)]
153 struct TestSink {
154 hits: Vec<RecordedHit>,
155 }
156
157 impl HitGraphSink for TestSink {
158 fn push_hit(
159 &mut self,
160 node_id: NodeId,
161 capture_path: &[NodeId],
162 geometry: HitGeometry,
163 _shape: Option<RoundedCornerShape>,
164 _click_actions: &[Rc<dyn Fn(Point)>],
165 _pointer_inputs: &[Rc<dyn Fn(PointerEvent)>],
166 ) {
167 self.hits.push((
168 node_id,
169 capture_path.to_vec(),
170 geometry.rect,
171 geometry.quad,
172 geometry.hit_clip_bounds,
173 geometry.hit_clips.len(),
174 ));
175 }
176 }
177
178 fn test_layer(node_id: NodeId, transform_to_parent: ProjectiveTransform) -> LayerNode {
179 LayerNode {
180 node_id: Some(node_id),
181 local_bounds: Rect {
182 x: 0.0,
183 y: 0.0,
184 width: 30.0,
185 height: 18.0,
186 },
187 transform_to_parent,
188 content_offset: cranpose_ui_graphics::Point::default(),
189 motion_context_animated: false,
190 translated_content_context: false,
191 translated_content_offset: cranpose_ui_graphics::Point::default(),
192 graphics_layer: cranpose_ui_graphics::GraphicsLayer::default(),
193 clip_to_bounds: true,
194 shadow_clip: None,
195 hit_test: Some(HitTestNode {
196 shape: None,
197 click_actions: vec![Rc::new(|_point| {})],
198 pointer_inputs: vec![],
199 clip: None,
200 }),
201 has_hit_targets: true,
202 isolation: IsolationReasons::default(),
203 cache_policy: CachePolicy::None,
204 cache_hashes: LayerRasterCacheHashes::default(),
205 cache_hashes_valid: false,
206 children: vec![],
207 }
208 }
209
210 #[test]
211 fn collect_hits_uses_graph_transform_to_parent() {
212 let layer = test_layer(7, ProjectiveTransform::translation(12.0, 9.0));
213 let mut sink = TestSink::default();
214
215 collect_hits_from_graph(&layer, ProjectiveTransform::identity(), &mut sink, None);
216
217 assert_eq!(sink.hits.len(), 1);
218 let (node_id, capture_path, rect, quad, clip, clip_count) = &sink.hits[0];
219 assert_eq!(*node_id, 7);
220 assert_eq!(capture_path, &vec![7]);
221 assert_eq!(
222 *rect,
223 Rect {
224 x: 12.0,
225 y: 9.0,
226 width: 30.0,
227 height: 18.0,
228 }
229 );
230 assert_eq!(
231 *quad,
232 [[12.0, 9.0], [42.0, 9.0], [12.0, 27.0], [42.0, 27.0]]
233 );
234 assert_eq!(*clip, Some(*rect));
235 assert_eq!(*clip_count, 1);
236 }
237
238 #[test]
239 fn collect_hits_composes_nested_graph_transforms() {
240 let child = test_layer(9, ProjectiveTransform::translation(4.0, 3.0));
241 let mut parent = test_layer(7, ProjectiveTransform::translation(10.0, 6.0));
242 parent.hit_test.as_mut().expect("hit test").pointer_inputs = vec![Rc::new(|_event| {})];
243 parent.children.push(RenderNode::Layer(Box::new(child)));
244 let mut sink = TestSink::default();
245
246 collect_hits_from_graph(&parent, ProjectiveTransform::identity(), &mut sink, None);
247
248 assert_eq!(sink.hits.len(), 2);
249 let (_, child_capture_path, child_rect, child_quad, child_clip, child_clip_count) =
250 &sink.hits[1];
251 assert_eq!(child_capture_path, &vec![9, 7]);
252 assert_eq!(
253 *child_rect,
254 Rect {
255 x: 14.0,
256 y: 9.0,
257 width: 30.0,
258 height: 18.0,
259 }
260 );
261 assert_eq!(
262 *child_quad,
263 [[14.0, 9.0], [44.0, 9.0], [14.0, 27.0], [44.0, 27.0]]
264 );
265 assert_eq!(
266 *child_clip,
267 Some(Rect {
268 x: 14.0,
269 y: 9.0,
270 width: 26.0,
271 height: 15.0,
272 })
273 );
274 assert_eq!(*child_clip_count, 2);
275 }
276
277 #[test]
278 fn collect_hits_retains_transformed_clip_chain() {
279 let mut parent = test_layer(1, ProjectiveTransform::translation(20.0, 10.0));
280 let mut child = test_layer(
281 2,
282 ProjectiveTransform::from_rect_to_quad(
283 Rect {
284 x: 0.0,
285 y: 0.0,
286 width: 30.0,
287 height: 18.0,
288 },
289 [[0.0, 0.0], [30.0, 0.0], [4.0, 18.0], [34.0, 18.0]],
290 ),
291 );
292 child.clip_to_bounds = true;
293 parent.children.push(RenderNode::Layer(Box::new(child)));
294
295 let mut sink = TestSink::default();
296 collect_hits_from_graph(&parent, ProjectiveTransform::identity(), &mut sink, None);
297
298 let (_, _, _, _, _, child_clip_count) = sink.hits[1];
299 assert_eq!(child_clip_count, 2);
300 }
301}