1use std::rc::Rc;
2
3use cranpose_core::NodeId;
4use cranpose_ui_graphics::{Rect, layer_transform::layer_surface};
5use smallvec::SmallVec;
6
7use crate::{
8 graph::{HitTestNode, LayerNode, ProjectiveTransform, RenderNode, quad_bounds},
9 graph_scene::{
10 HitClip, HitGeometry, HitTargetSpec, HitTargets, Scene, SurfaceGeometry, SurfaceTarget,
11 },
12 primitive_emit::resolve_clip,
13 scene_pick::ScenePick,
14};
15
16pub trait HitGraphSink {
18 fn push_hit(
21 &mut self,
22 node_id: NodeId,
23 capture_path: &[NodeId],
24 geometry: HitGeometry<'_>,
25 hit: &HitTestNode,
26 );
27}
28
29impl HitGraphSink for Scene {
30 fn push_hit(
31 &mut self,
32 node_id: NodeId,
33 capture_path: &[NodeId],
34 geometry: HitGeometry<'_>,
35 hit: &HitTestNode,
36 ) {
37 Scene::push_hit(self, node_id, capture_path, geometry, hit_spec(hit));
38 }
39}
40
41impl HitGraphSink for HitTargets<'_> {
42 fn push_hit(
43 &mut self,
44 node_id: NodeId,
45 capture_path: &[NodeId],
46 geometry: HitGeometry<'_>,
47 hit: &HitTestNode,
48 ) {
49 self.push(node_id, capture_path, geometry, hit_spec(hit));
50 }
51}
52
53fn hit_spec(hit: &HitTestNode) -> HitTargetSpec<'_> {
56 HitTargetSpec {
57 shape: None,
58 handlers: &hit.handlers,
59 }
60}
61
62pub fn collect_hits_from_graph<S: HitGraphSink>(
67 layer: &LayerNode,
68 parent_transform: ProjectiveTransform,
69 sink: &mut S,
70 parent_hit_clip: Option<Rect>,
71) {
72 if !layer.has_hit_targets {
73 return;
74 }
75 let mut walk = HitWalk {
76 sink,
77 hit_clips: Vec::new(),
78 pointer_input_ancestors: Vec::new(),
79 capture_path: SmallVec::new(),
80 surface: None,
81 };
82 walk.visit(
83 layer,
84 parent_transform,
85 parent_hit_clip,
86 &ParentSpace::default(),
87 );
88}
89
90struct HitWalk<'s, S> {
91 sink: &'s mut S,
92 hit_clips: Vec<HitClip>,
93 pointer_input_ancestors: Vec<NodeId>,
94 capture_path: SmallVec<[NodeId; 8]>,
95 surface: Option<SurfaceWalk>,
98}
99
100struct SurfaceWalk {
101 target: Rc<SurfaceTarget>,
102 clips: Vec<HitClip>,
103}
104
105#[derive(Default)]
111struct ParentSpace {
112 scene: Option<Rc<ScenePick>>,
113 scene_clips: (Option<Rect>, usize),
114 content: Option<ProjectiveTransform>,
115 content_clip: Option<Rect>,
116 behind_doorway: bool,
119 unseen: bool,
122}
123
124impl<S: HitGraphSink> HitWalk<'_, S> {
125 fn visit(
126 &mut self,
127 layer: &LayerNode,
128 parent_transform: ProjectiveTransform,
129 parent_hit_clip: Option<Rect>,
130 space: &ParentSpace,
131 ) {
132 if !layer.has_hit_targets {
133 return;
134 }
135 let Some((doorway, unseen)) = Self::seen(layer, space) else {
137 return;
138 };
139 let transform = layer.transform_to_parent.then(parent_transform);
140 let node_rect = layer.node_rect();
143 let transformed_quad = hit_quad(&transform, node_rect);
144 let transformed_rect = quad_bounds(transformed_quad);
145
146 let in_scene = space.scene.is_some();
150 if !in_scene && (transformed_rect.width <= 0.0 || transformed_rect.height <= 0.0) {
151 return;
152 }
153 let world_to_local = match transform.inverse() {
154 Some(inverse) => inverse,
155 None if in_scene => ProjectiveTransform::identity(),
156 None => return,
157 };
158
159 let (entered, content, content_clip) = self.enter_content(layer, (space, doorway));
160 let behind_doorway = space.behind_doorway || doorway;
161 let Some((hit_clip_bounds, pushed_clip)) =
162 self.push_window_clip(layer, &transform, (parent_hit_clip, in_scene))
163 else {
164 self.leave(entered, content_clip.pushed);
165 return;
166 };
167
168 if let (Some(node_id), Some(hit), false) = (
169 layer.node_id,
170 &layer.hit_test,
171 self.hides_targets(unseen, behind_doorway),
172 ) {
173 let geometry = PlacedNode {
174 rect: transformed_rect,
175 quad: transformed_quad,
176 local_bounds: node_rect,
177 world_to_local,
178 hit_clip_bounds,
179 content,
180 content_clip: content_clip.bounds,
181 };
182 self.push_node(node_id, hit, geometry);
183 }
184
185 let pointer_input_ancestor = layer
186 .hit_test
187 .as_ref()
188 .filter(|hit| !hit.handlers.pointer_inputs().is_empty())
189 .and(layer.node_id);
190 if let Some(node_id) = pointer_input_ancestor {
191 self.pointer_input_ancestors.push(node_id);
192 }
193
194 let children_space = ParentSpace {
195 content,
196 content_clip: content_clip.bounds,
197 behind_doorway,
198 unseen,
199 ..self.children_scene(layer, (transform, hit_clip_bounds), space)
200 };
201 for child in &layer.children {
202 if let RenderNode::Layer(child_layer) = child {
203 self.visit(child_layer, transform, hit_clip_bounds, &children_space);
204 }
205 }
206
207 if pointer_input_ancestor.is_some() {
208 let _ = self.pointer_input_ancestors.pop();
209 }
210
211 if pushed_clip {
212 let _ = self.hit_clips.pop();
213 }
214 self.leave(entered, content_clip.pushed);
215 }
216
217 fn push_node(&mut self, node_id: NodeId, hit: &HitTestNode, node: PlacedNode) {
220 self.capture_path.clear();
221 self.capture_path.push(node_id);
222 self.capture_path
223 .extend(self.pointer_input_ancestors.iter().rev().copied());
224 let content_to_local = node.content.and_then(ProjectiveTransform::inverse);
225 let surface =
226 self.surface
227 .as_ref()
228 .zip(content_to_local)
229 .map(|(surface, content_to_local)| SurfaceGeometry {
230 target: &surface.target,
231 content_to_local,
232 clip_bounds: node.content_clip,
233 clips: &surface.clips,
234 });
235 self.sink.push_hit(
236 node_id,
237 &self.capture_path,
238 HitGeometry {
239 rect: node.rect,
240 quad: node.quad,
241 local_bounds: node.local_bounds,
242 world_to_local: node.world_to_local,
243 hit_clip_bounds: node.hit_clip_bounds,
244 hit_clips: &self.hit_clips,
245 surface,
246 },
247 hit,
248 );
249 }
250
251 fn children_scene(
256 &self,
257 layer: &LayerNode,
258 (transform, hit_clip_bounds): (ProjectiveTransform, Option<Rect>),
259 space: &ParentSpace,
260 ) -> ParentSpace {
261 let through_portal = space
262 .scene
263 .as_ref()
264 .is_some_and(|scene| scene.holds_scene(layer));
265 let shown = match (layer.wraps, through_portal) {
266 (None, false) => ScenePick::of(layer, transform).map(Rc::new),
267 _ => None,
268 };
269 let (scene, scene_clips) = match shown {
270 Some(pick) => (Some(pick), (hit_clip_bounds, self.hit_clips.len())),
271 None => (space.scene.clone(), space.scene_clips),
272 };
273 ParentSpace {
274 scene,
275 scene_clips,
276 ..ParentSpace::default()
277 }
278 }
279
280 fn enter_surface(
284 &mut self,
285 layer: &LayerNode,
286 space: &ParentSpace,
287 ) -> Option<Option<SurfaceWalk>> {
288 let pick = space.scene.as_ref()?;
289 let index = pick.index_of(layer)?;
290 let (outer_clip_bounds, outer_clip_count) = space.scene_clips;
291 let target = Rc::new(SurfaceTarget {
292 pick: Rc::clone(pick),
293 index,
294 outer_clip_bounds,
295 outer_clips: self.hit_clips[..outer_clip_count.min(self.hit_clips.len())].to_vec(),
296 });
297 Some(self.surface.replace(SurfaceWalk {
298 target,
299 clips: Vec::new(),
300 }))
301 }
302
303 fn doorway(layer: &LayerNode, space: &ParentSpace) -> Option<bool> {
306 let Some(pick) = space.scene.as_ref().filter(|pick| pick.holds_scene(layer)) else {
307 return Some(false);
308 };
309 pick.sees_scene(layer).then_some(true)
310 }
311
312 fn seen(layer: &LayerNode, space: &ParentSpace) -> Option<(bool, bool)> {
317 let doorway = Self::doorway(layer, space)?;
318 let unseen = !doorway && (space.unseen || Self::unseen_surface(layer, space));
319 Some((doorway, unseen))
320 }
321
322 fn hides_targets(&self, unseen: bool, behind_doorway: bool) -> bool {
325 unseen || (behind_doorway && self.surface.is_none())
326 }
327
328 fn unseen_surface(layer: &LayerNode, space: &ParentSpace) -> bool {
331 space.scene.as_ref().is_some_and(|pick| {
332 layer_surface(&layer.graphics_layer).is_some() && pick.index_of(layer).is_none()
333 })
334 }
335
336 fn enter_content(
342 &mut self,
343 layer: &LayerNode,
344 (space, doorway): (&ParentSpace, bool),
345 ) -> (
346 Option<Option<SurfaceWalk>>,
347 Option<ProjectiveTransform>,
348 ContentClip,
349 ) {
350 if doorway {
351 let unclipped = ContentClip {
352 bounds: None,
353 pushed: false,
354 };
355 return (Some(self.surface.take()), None, unclipped);
356 }
357 let entered = self.enter_surface(layer, space);
358 let (content, above) = match entered {
361 Some(_) => (Some(ProjectiveTransform::identity()), None),
362 None => (
363 space
364 .content
365 .map(|parent| layer.transform_to_parent.then(parent)),
366 space.content_clip,
367 ),
368 };
369 let content_clip = self.push_content_clip(layer, content, above);
370 (entered, content, content_clip)
371 }
372
373 fn push_window_clip(
379 &mut self,
380 layer: &LayerNode,
381 transform: &ProjectiveTransform,
382 (parent_hit_clip, in_scene): (Option<Rect>, bool),
383 ) -> Option<(Option<Rect>, bool)> {
384 let Some(local_clip) = layer.clip_rect().filter(|_| !in_scene) else {
385 return Some((parent_hit_clip, false));
386 };
387 let quad = hit_quad(transform, local_clip);
388 let bounds = quad_bounds(quad);
389 let resolved = resolve_clip(parent_hit_clip, Some(bounds));
390 if resolved.is_some_and(|clip| clip.is_empty()) {
391 return None;
392 }
393 self.hit_clips.push(HitClip { quad, bounds });
394 Some((resolved, true))
395 }
396
397 fn push_content_clip(
400 &mut self,
401 layer: &LayerNode,
402 content: Option<ProjectiveTransform>,
403 parent_clip: Option<Rect>,
404 ) -> ContentClip {
405 let unclipped = ContentClip {
406 bounds: parent_clip,
407 pushed: false,
408 };
409 let (Some(surface), Some(content)) = (self.surface.as_mut(), content) else {
410 return unclipped;
411 };
412 let Some(local_clip) = layer.clip_rect() else {
413 return unclipped;
414 };
415 let quad = hit_quad(&content, local_clip);
416 let bounds = quad_bounds(quad);
417 surface.clips.push(HitClip { quad, bounds });
418 ContentClip {
419 bounds: resolve_clip(parent_clip, Some(bounds)),
420 pushed: true,
421 }
422 }
423
424 fn leave(&mut self, entered: Option<Option<SurfaceWalk>>, pushed_content_clip: bool) {
425 if pushed_content_clip && let Some(surface) = self.surface.as_mut() {
426 let _ = surface.clips.pop();
427 }
428 if let Some(previous) = entered {
429 self.surface = previous;
430 }
431 }
432}
433
434struct PlacedNode {
437 rect: Rect,
438 quad: [[f32; 2]; 4],
439 local_bounds: Rect,
440 world_to_local: ProjectiveTransform,
441 hit_clip_bounds: Option<Rect>,
442 content: Option<ProjectiveTransform>,
443 content_clip: Option<Rect>,
444}
445
446struct ContentClip {
447 bounds: Option<Rect>,
448 pushed: bool,
449}
450
451fn hit_quad(transform: &ProjectiveTransform, rect: Rect) -> [[f32; 2]; 4] {
452 if transform.rect_in_front(rect) {
453 return transform.map_rect(rect);
454 }
455 let bounds = transform.bounds_for_rect(rect);
456 let (right, bottom) = (bounds.x + bounds.width, bounds.y + bounds.height);
457 [
458 [bounds.x, bounds.y],
459 [right, bounds.y],
460 [bounds.x, bottom],
461 [right, bottom],
462 ]
463}
464
465#[cfg(test)]
466#[path = "tests/hit_graph_tests.rs"]
467mod tests;