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