Skip to main content

cranpose_render_common/
hit_graph.rs

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
16/// Receives the hit targets that `collect_hits_from_graph` finds in a render graph.
17pub trait HitGraphSink {
18    /// Records an interactive node. `capture_path` lists the node first and then its pointer-input
19    /// ancestors from the nearest outward, and `geometry` is in window space.
20    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
53/// What a graph's hit test node pushes as a target: its handlers, with no
54/// shape of its own.
55fn hit_spec(hit: &HitTestNode) -> HitTargetSpec<'_> {
56    HitTargetSpec {
57        shape: None,
58        handlers: &hit.handlers,
59    }
60}
61
62/// Walks `layer` and the layers beneath it in draw order and pushes to `sink` a hit target for
63/// each node that has a hit test, with its geometry in window space. `parent_transform` maps
64/// the parent layer's space to the window, and `parent_hit_clip` is the clip bounds that the
65/// layers above impose.
66pub 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    /// The mesh surface the walk is inside, and the clips cutting its
96    /// content, in content space.
97    surface: Option<SurfaceWalk>,
98}
99
100struct SurfaceWalk {
101    target: Rc<SurfaceTarget>,
102    clips: Vec<HitClip>,
103}
104
105/// What a layer's parent places it in: the scene above it, with the clip
106/// bounds and the number of window clips that cut the scene's surfaces, and
107/// inside a mesh surface, the map from the parent to the surface's content
108/// with the content clip above. A surface that joins the scene from inside
109/// another's content is cut only where the scene's surfaces are.
110#[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    /// Whether the layer lies in a space behind a doorway, where only the
117    /// content of its mesh surfaces shows and takes input.
118    behind_doorway: bool,
119    /// Whether the layer lies on a surface the eye does not see, whose
120    /// content takes no input.
121    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        // Behind a doorway the eye does not see through, nothing takes input.
136        let Some((doorway, unseen)) = Self::seen(layer, space) else {
137            return;
138        };
139        let transform = layer.transform_to_parent.then(parent_transform);
140        // The node's handlers sit on its own rect; its clip, at its layer's
141        // bounds, cuts them as it cuts what the layer holds.
142        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        // Inside a scene the camera's pick decides where the surfaces'
147        // content takes input, whatever their flat stand-ins show: an eye
148        // behind a wall still sees what lies through its doorway.
149        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    /// Records a node's hit target with its ancestors' clips and, inside a
218    /// surface, where it lies on the surface's content.
219    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    /// The scene above `layer`'s children: the one `layer` shows, cut by the
252    /// clips above them, or the one above `layer`, which also finds the
253    /// surfaces of a scene `layer` shows through one of its portals; the
254    /// content fields stay empty.
255    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    /// Starts the surface `layer` is, when the scene above draws it, cut by
281    /// the clips that cut the scene's surfaces; returns the surface walk it
282    /// replaces.
283    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    /// Whether `layer` shows a scene behind a doorway of the scene above;
304    /// `None` when the eye does not see through that doorway.
305    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    /// Whether `layer` shows a scene behind a doorway of the scene above,
313    /// and whether it lies on a surface the eye does not see, whose content
314    /// takes no input but for what the eye sees through a doorway in it;
315    /// `None` when the eye does not see through the doorway `layer` is.
316    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    /// Whether a layer pushes no hit target: it lies on a surface the eye
323    /// does not see, or behind a doorway outside any surface's content.
324    fn hides_targets(&self, unseen: bool, behind_doorway: bool) -> bool {
325        unseen || (behind_doorway && self.surface.is_none())
326    }
327
328    /// Whether `layer` is a mesh surface of the scene above that the eye
329    /// does not see: it lies in a space the eye does not see.
330    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    /// Starts the surface `layer` is, if the scene above draws it, or sets
337    /// aside the surface the walk is inside for a `doorway` into a space of
338    /// its own, and returns the surface walk it replaces, the map from
339    /// `layer` to the content of the surface the walk is inside, and the
340    /// content clip that cuts `layer`'s subtree.
341    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        // A surface's content starts a space of its own, which the clips of
359        // the content above do not cut.
360        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    /// Pushes `layer`'s clip in the window, which `transform` maps the layer
374    /// to: the clip bounds that cut its subtree and whether it pushed one;
375    /// `None` when the clip leaves nothing. Inside a scene a layer's clip
376    /// cuts in its surface's content instead, not on the window, where a
377    /// surface behind the eye shows nothing.
378    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    /// Pushes `layer`'s clip in its surface's content space, when the walk
398    /// is inside a surface.
399    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
434/// Where a node's own rect lies: in the window, and inside a surface, on
435/// the surface's content.
436struct 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;