Skip to main content

cranpose_render_common/
hit_graph.rs

1use 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;