cranpose_render_common/
hit_graph.rs1use cranpose_core::NodeId;
2use cranpose_ui_graphics::Rect;
3use smallvec::SmallVec;
4
5use crate::{
6 graph::{HitTestNode, LayerNode, ProjectiveTransform, RenderNode, quad_bounds},
7 graph_scene::{ClickAction, HitClip, HitGeometry, HitTargetSpec, Scene},
8 primitive_emit::resolve_clip,
9};
10
11pub trait HitGraphSink {
12 fn push_hit(
13 &mut self,
14 node_id: NodeId,
15 capture_path: &[NodeId],
16 geometry: HitGeometry<'_>,
17 hit: &HitTestNode,
18 );
19}
20
21impl HitGraphSink for Scene {
22 fn push_hit(
23 &mut self,
24 node_id: NodeId,
25 capture_path: &[NodeId],
26 geometry: HitGeometry<'_>,
27 hit: &HitTestNode,
28 ) {
29 Scene::push_hit(
30 self,
31 node_id,
32 capture_path,
33 geometry,
34 HitTargetSpec {
35 shape: hit.shape,
36 click_actions: hit
37 .click_actions
38 .iter()
39 .cloned()
40 .map(ClickAction::WithPoint),
41 pointer_inputs: &hit.pointer_inputs,
42 pointer_icon: hit.pointer_icon.as_ref(),
43 },
44 );
45 }
46}
47
48pub fn collect_hits_from_graph<S: HitGraphSink>(
49 layer: &LayerNode,
50 parent_transform: ProjectiveTransform,
51 sink: &mut S,
52 parent_hit_clip: Option<Rect>,
53) {
54 if !layer.has_hit_targets {
55 return;
56 }
57 let mut hit_clips = Vec::new();
58 let mut pointer_input_ancestors = Vec::new();
59 let mut capture_path = SmallVec::<[NodeId; 8]>::new();
60 collect_hits_from_graph_inner(
61 layer,
62 parent_transform,
63 sink,
64 parent_hit_clip,
65 &mut hit_clips,
66 &mut pointer_input_ancestors,
67 &mut capture_path,
68 );
69}
70
71fn collect_hits_from_graph_inner<S: HitGraphSink>(
72 layer: &LayerNode,
73 parent_transform: ProjectiveTransform,
74 sink: &mut S,
75 parent_hit_clip: Option<Rect>,
76 hit_clips: &mut Vec<HitClip>,
77 pointer_input_ancestors: &mut Vec<NodeId>,
78 capture_path: &mut SmallVec<[NodeId; 8]>,
79) {
80 if !layer.has_hit_targets {
81 return;
82 }
83 let transform = layer.transform_to_parent.then(parent_transform);
84 let transformed_quad = transform.map_rect(layer.local_bounds);
85 let transformed_rect = quad_bounds(transformed_quad);
86
87 if transformed_rect.width <= 0.0 || transformed_rect.height <= 0.0 {
88 return;
89 }
90
91 let Some(world_to_local) = transform.inverse() else {
92 return;
93 };
94
95 let mut hit_clip_bounds = parent_hit_clip;
96 let mut pushed_clip = false;
97 if let Some(local_clip) = layer.clip_rect() {
98 let clip_quad = transform.map_rect(local_clip);
99 let clip_bounds = quad_bounds(clip_quad);
100 let resolved_clip_bounds = resolve_clip(parent_hit_clip, Some(clip_bounds));
101 if resolved_clip_bounds.is_some_and(|clip| clip.is_empty()) {
102 return;
103 }
104 hit_clip_bounds = 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 capture_path.clear();
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,
126 },
127 hit,
128 );
129 }
130
131 let pointer_input_ancestor = layer
132 .hit_test
133 .as_ref()
134 .filter(|hit| !hit.pointer_inputs.is_empty())
135 .and(layer.node_id);
136 if let Some(node_id) = pointer_input_ancestor {
137 pointer_input_ancestors.push(node_id);
138 }
139
140 for child in &layer.children {
141 if let RenderNode::Layer(child_layer) = child {
142 collect_hits_from_graph_inner(
143 child_layer,
144 transform,
145 sink,
146 hit_clip_bounds,
147 hit_clips,
148 pointer_input_ancestors,
149 capture_path,
150 );
151 }
152 }
153
154 if pointer_input_ancestor.is_some() {
155 let _ = pointer_input_ancestors.pop();
156 }
157
158 if pushed_clip {
159 let _ = hit_clips.pop();
160 }
161}
162
163#[cfg(test)]
164#[path = "tests/hit_graph_tests.rs"]
165mod tests;