Skip to main content

ferroui_base/rendering/
managed_hit_tester.rs

1use super::IHitTester;
2use crate::media::{Geometry, GeometryHitTestResult, IntersectionResult, MatrixTransform, RectangleGeometry};
3use crate::visual_tree::local_transform;
4use crate::{Matrix, Point, Rect, Ref, Visual};
5
6/// A hit tester that walks the visual tree directly.
7///
8/// A visual is hit when the point lies within its bounds. Subtrees are
9/// skipped when the visual is invisible, rejected by the filter, or clips
10/// its content to its bounds and the point lies outside of them, or has a
11/// clip geometry that does not contain the point. Children
12/// are tested before their parent and in front-to-back order: by descending
13/// Z index, then by descending position in the visual children.
14///
15/// Hit testing with a geometry follows the same rules with the geometry in
16/// place of the point: a visual is hit when the rectangle of its bounds
17/// and the geometry intersect, and the result tells how. It needs the
18/// geometry implementations of the render interface.
19///
20/// This is the fallback for presentation sources without a renderer-backed
21/// hit tester. It only knows about bounds: it does not look at what a
22/// visual actually draws, so a visual is hit anywhere within its bounds
23/// even where it draws nothing.
24#[derive(Clone, Copy, Debug, Default)]
25pub struct ManagedHitTester;
26
27impl ManagedHitTester {
28    /// Creates the hit tester.
29    pub const fn new() -> Self {
30        Self
31    }
32
33    /// Visits the visuals at `p` (in the coordinates of `visual`), topmost
34    /// first. Returns true if `visit` asked to stop.
35    fn hit_test_core(
36        visual: &Visual,
37        p: Point,
38        filter: Option<&dyn Fn(&Visual) -> bool>,
39        visit: &mut dyn FnMut(&Visual) -> bool,
40    ) -> bool {
41        if !visual.is_visible() {
42            return false;
43        }
44
45        if let Some(filter) = filter {
46            if !filter(visual) {
47                return false;
48            }
49        }
50
51        let contains_point = Rect::from_size(visual.bounds().size()).contains(p);
52
53        if visual.clip_to_bounds() && !contains_point {
54            return false;
55        }
56
57        if let Some(clip) = visual.clip() {
58            if !clip.fill_contains(p) {
59                return false;
60            }
61        }
62
63        if let Some(children) = visual.visual_children_snapshot() {
64            if visual.has_non_uniform_z_index_children() {
65                for child in Visual::sort_by_z_index(&children).iter() {
66                    if Self::hit_test_child(child, p, filter, visit) {
67                        return true;
68                    }
69                }
70            } else {
71                for child in children.iter().rev() {
72                    if Self::hit_test_child(child, p, filter, visit) {
73                        return true;
74                    }
75                }
76            }
77        }
78
79        // A visual with a custom hit test decides for itself.
80        let is_hit = visual.custom_hit_test(p).unwrap_or(contains_point);
81
82        is_hit && visit(visual)
83    }
84
85    fn hit_test_child(
86        child: &Visual,
87        parent_point: Point,
88        filter: Option<&dyn Fn(&Visual) -> bool>,
89        visit: &mut dyn FnMut(&Visual) -> bool,
90    ) -> bool {
91        match local_transform(child).try_invert() {
92            Some(inverted) => Self::hit_test_core(child, parent_point.transform(inverted), filter, visit),
93            None => false,
94        }
95    }
96
97    /// Returns `input` in the coordinate space that `matrix` maps the
98    /// current one to.
99    fn transform_geometry(input: &Geometry, matrix: Matrix) -> Ref<Geometry> {
100        let result = input.clone_geometry();
101        let current = input.transform().map_or(Matrix::IDENTITY, |transform| transform.value());
102        result.set_transform(MatrixTransform::with_matrix(current * matrix));
103        result
104    }
105
106    fn is_hit(result: IntersectionResult) -> bool {
107        !matches!(result, IntersectionResult::NotCalculated | IntersectionResult::Empty)
108    }
109
110    /// Visits the visuals intersecting `geometry` (in the coordinates of
111    /// `visual`), topmost first. Returns true if `visit` asked to stop.
112    fn hit_test_geometry_core(
113        visual: &Visual,
114        geometry: &Geometry,
115        filter: Option<&dyn Fn(&Visual) -> bool>,
116        visit: &mut dyn FnMut(&Visual, IntersectionResult) -> bool,
117    ) -> bool {
118        if !visual.is_visible() {
119            return false;
120        }
121
122        if let Some(filter) = filter {
123            if !filter(visual) {
124                return false;
125            }
126        }
127
128        let bounds = Rect::from_size(visual.bounds().size());
129
130        if visual.clip_to_bounds() {
131            let geometry_bounds = geometry.bounds();
132            if !(geometry_bounds.width > 0.0 && geometry_bounds.height > 0.0 && geometry_bounds.intersects(bounds)) {
133                return false;
134            }
135        }
136
137        if let Some(clip) = visual.clip() {
138            if !clip.get_fill_intersection_result(geometry).is_some_and(Self::is_hit) {
139                return false;
140            }
141        }
142
143        if let Some(children) = visual.visual_children_snapshot() {
144            if visual.has_non_uniform_z_index_children() {
145                for child in Visual::sort_by_z_index(&children).iter() {
146                    if Self::hit_test_geometry_child(child, geometry, filter, visit) {
147                        return true;
148                    }
149                }
150            } else {
151                for child in children.iter().rev() {
152                    if Self::hit_test_geometry_child(child, geometry, filter, visit) {
153                        return true;
154                    }
155                }
156            }
157        }
158
159        // The relation between the hit geometry and the rectangle the
160        // visual occupies.
161        if let Some(result) = visual.custom_hit_test_geometry(&geometry.to_ref()) {
162            return Self::is_hit(result) && visit(visual, result);
163        }
164
165        let target = RectangleGeometry::with_rect(bounds);
166        match geometry.get_fill_intersection_result(&target) {
167            Some(result) if Self::is_hit(result) => visit(visual, result),
168            _ => false,
169        }
170    }
171
172    fn hit_test_geometry_child(
173        child: &Visual,
174        parent_geometry: &Geometry,
175        filter: Option<&dyn Fn(&Visual) -> bool>,
176        visit: &mut dyn FnMut(&Visual, IntersectionResult) -> bool,
177    ) -> bool {
178        match local_transform(child).try_invert() {
179            Some(inverted) => {
180                let geometry = Self::transform_geometry(parent_geometry, inverted);
181                Self::hit_test_geometry_core(child, &geometry, filter, visit)
182            }
183            None => false,
184        }
185    }
186}
187
188impl IHitTester for ManagedHitTester {
189    fn hit_test(&self, p: Point, root: &Visual, filter: Option<&dyn Fn(&Visual) -> bool>) -> Vec<Ref<Visual>> {
190        let mut result = Vec::new();
191        Self::hit_test_core(root, p, filter, &mut |visual| {
192            result.push(visual.to_ref());
193            false
194        });
195        result
196    }
197
198    fn hit_test_first(
199        &self,
200        p: Point,
201        root: &Visual,
202        filter: Option<&dyn Fn(&Visual) -> bool>,
203    ) -> Option<Ref<Visual>> {
204        let mut result = None;
205        Self::hit_test_core(root, p, filter, &mut |visual| {
206            result = Some(visual.to_ref());
207            true
208        });
209        result
210    }
211
212    fn hit_test_geometry(
213        &self,
214        geometry: &Geometry,
215        root: &Visual,
216        filter: Option<&dyn Fn(&Visual) -> bool>,
217    ) -> Vec<GeometryHitTestResult> {
218        let mut result = Vec::new();
219        Self::hit_test_geometry_core(root, geometry, filter, &mut |visual, intersection| {
220            result.push(GeometryHitTestResult::new(visual.to_ref(), intersection));
221            false
222        });
223        result
224    }
225
226    fn hit_test_first_geometry(
227        &self,
228        geometry: &Geometry,
229        root: &Visual,
230        filter: Option<&dyn Fn(&Visual) -> bool>,
231    ) -> Option<GeometryHitTestResult> {
232        let mut result = None;
233        Self::hit_test_geometry_core(root, geometry, filter, &mut |visual, intersection| {
234            result = Some(GeometryHitTestResult::new(visual.to_ref(), intersection));
235            true
236        });
237        result
238    }
239}