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::{HitClip, HitGeometry, HitTargetSpec, Scene},
8    primitive_emit::resolve_clip,
9};
10
11/// Receives the hit targets that `collect_hits_from_graph` finds in a render graph.
12pub trait HitGraphSink {
13    /// Records an interactive node. `capture_path` lists the node first and then its pointer-input
14    /// ancestors from the nearest outward, and `geometry` is in window space.
15    fn push_hit(
16        &mut self,
17        node_id: NodeId,
18        capture_path: &[NodeId],
19        geometry: HitGeometry<'_>,
20        hit: &HitTestNode,
21    );
22}
23
24impl HitGraphSink for Scene {
25    fn push_hit(
26        &mut self,
27        node_id: NodeId,
28        capture_path: &[NodeId],
29        geometry: HitGeometry<'_>,
30        hit: &HitTestNode,
31    ) {
32        Scene::push_hit(
33            self,
34            node_id,
35            capture_path,
36            geometry,
37            HitTargetSpec {
38                shape: None,
39                handlers: &hit.handlers,
40            },
41        );
42    }
43}
44
45/// Walks `layer` and the layers beneath it in draw order and pushes to `sink` a hit target for
46/// each node that has a hit test, with its geometry in window space. `parent_transform` maps
47/// the parent layer's space to the window, and `parent_hit_clip` is the clip bounds that the
48/// layers above impose.
49pub fn collect_hits_from_graph<S: HitGraphSink>(
50    layer: &LayerNode,
51    parent_transform: ProjectiveTransform,
52    sink: &mut S,
53    parent_hit_clip: Option<Rect>,
54) {
55    if !layer.has_hit_targets {
56        return;
57    }
58    let mut hit_clips = Vec::new();
59    let mut pointer_input_ancestors = Vec::new();
60    let mut capture_path = SmallVec::<[NodeId; 8]>::new();
61    collect_hits_from_graph_inner(
62        layer,
63        parent_transform,
64        sink,
65        parent_hit_clip,
66        &mut hit_clips,
67        &mut pointer_input_ancestors,
68        &mut capture_path,
69    );
70}
71
72fn collect_hits_from_graph_inner<S: HitGraphSink>(
73    layer: &LayerNode,
74    parent_transform: ProjectiveTransform,
75    sink: &mut S,
76    parent_hit_clip: Option<Rect>,
77    hit_clips: &mut Vec<HitClip>,
78    pointer_input_ancestors: &mut Vec<NodeId>,
79    capture_path: &mut SmallVec<[NodeId; 8]>,
80) {
81    if !layer.has_hit_targets {
82        return;
83    }
84    let transform = layer.transform_to_parent.then(parent_transform);
85    // The node's handlers sit on its own rect; its clip, at its layer's
86    // bounds, cuts them as it cuts what the layer holds.
87    let node_rect = layer.node_rect();
88    let transformed_quad = transform.map_rect(node_rect);
89    let transformed_rect = quad_bounds(transformed_quad);
90
91    if transformed_rect.width <= 0.0 || transformed_rect.height <= 0.0 {
92        return;
93    }
94
95    let Some(world_to_local) = transform.inverse() else {
96        return;
97    };
98
99    let mut hit_clip_bounds = parent_hit_clip;
100    let mut pushed_clip = false;
101    if let Some(local_clip) = layer.clip_rect() {
102        let clip_quad = transform.map_rect(local_clip);
103        let clip_bounds = quad_bounds(clip_quad);
104        let resolved_clip_bounds = resolve_clip(parent_hit_clip, Some(clip_bounds));
105        if resolved_clip_bounds.is_some_and(|clip| clip.is_empty()) {
106            return;
107        }
108        hit_clip_bounds = resolved_clip_bounds;
109        hit_clips.push(HitClip {
110            quad: clip_quad,
111            bounds: clip_bounds,
112        });
113        pushed_clip = true;
114    }
115
116    if let (Some(node_id), Some(hit)) = (layer.node_id, &layer.hit_test) {
117        capture_path.clear();
118        capture_path.push(node_id);
119        capture_path.extend(pointer_input_ancestors.iter().rev().copied());
120        sink.push_hit(
121            node_id,
122            capture_path,
123            HitGeometry {
124                rect: transformed_rect,
125                quad: transformed_quad,
126                local_bounds: node_rect,
127                world_to_local,
128                hit_clip_bounds,
129                hit_clips,
130            },
131            hit,
132        );
133    }
134
135    let pointer_input_ancestor = layer
136        .hit_test
137        .as_ref()
138        .filter(|hit| !hit.handlers.pointer_inputs().is_empty())
139        .and(layer.node_id);
140    if let Some(node_id) = pointer_input_ancestor {
141        pointer_input_ancestors.push(node_id);
142    }
143
144    for child in &layer.children {
145        if let RenderNode::Layer(child_layer) = child {
146            collect_hits_from_graph_inner(
147                child_layer,
148                transform,
149                sink,
150                hit_clip_bounds,
151                hit_clips,
152                pointer_input_ancestors,
153                capture_path,
154            );
155        }
156    }
157
158    if pointer_input_ancestor.is_some() {
159        let _ = pointer_input_ancestors.pop();
160    }
161
162    if pushed_clip {
163        let _ = hit_clips.pop();
164    }
165}
166
167#[cfg(test)]
168#[path = "tests/hit_graph_tests.rs"]
169mod tests;