Skip to main content

ferroui_base/rendering/composition/drawing/
render_data_stream_hit_test.rs

1use super::{IRenderDataVisitor, RenderDataStream};
2use crate::media::{
3    BoxShadows, CombinedGeometry, EllipseGeometry, Geometry, IBrush, IPen, ImmutableGeometry,
4    IntersectionResult, LineGeometry, MatrixTransform, RectangleGeometry, RenderOptions, TextOptions,
5};
6use crate::platform::{IGeometryImpl, IGlyphRunImpl};
7use crate::rendering::scene_graph::ICustomDrawOperation;
8use crate::{Matrix, Point, Rect, Ref, RoundedRect};
9use std::sync::Arc;
10
11/// What a hit-tested push has to restore.
12pub struct HitTestScope {
13    saved_live: bool,
14    restore_point: bool,
15    saved_point: Option<Point>,
16}
17
18impl HitTestScope {
19    fn new(saved_live: bool) -> Self {
20        Self { saved_live, restore_point: false, saved_point: None }
21    }
22}
23
24/// Hit tests recorded operations against a point or a geometry.
25pub struct HitTestVisitor {
26    stop_visiting: bool,
27    pub hit_found: bool,
28    pub hit_result: IntersectionResult,
29    current_geometry: Option<Ref<Geometry>>,
30    pub current_point: Option<Point>,
31    pub live: bool,
32    saved_geometries: Option<Vec<Ref<Geometry>>>,
33}
34
35impl HitTestVisitor {
36    fn new() -> Self {
37        Self {
38            stop_visiting: false,
39            hit_found: false,
40            hit_result: IntersectionResult::NotCalculated,
41            current_geometry: None,
42            current_point: None,
43            live: true,
44            saved_geometries: None,
45        }
46    }
47
48    pub fn for_geometry(geometry: Ref<Geometry>) -> Self {
49        Self { current_geometry: Some(geometry), saved_geometries: Some(Vec::new()), ..Self::new() }
50    }
51
52    pub fn for_point(point: Point) -> Self {
53        Self { current_point: Some(point), ..Self::new() }
54    }
55
56    fn hit(&mut self) {
57        self.hit_found = true;
58        self.stop_visiting = true;
59    }
60
61    fn hit_with(&mut self, result: IntersectionResult) {
62        self.hit_result = result;
63        self.stop_visiting = true;
64    }
65
66    fn get_intersection_detail(first_rect: Rect, second_rect: Rect) -> IntersectionResult {
67        if first_rect.contains_rect(second_rect) {
68            return IntersectionResult::FullyContains;
69        }
70        // Mirrors upstream, which compares the second rectangle with itself
71        // here.
72        if second_rect.contains_rect(second_rect) {
73            return IntersectionResult::FullyInside;
74        }
75        if first_rect.intersects(second_rect) {
76            return IntersectionResult::Intersects;
77        }
78        IntersectionResult::Empty
79    }
80}
81
82impl IRenderDataVisitor for HitTestVisitor {
83    type Scope = HitTestScope;
84
85    fn stop_visiting(&self) -> bool {
86        self.stop_visiting
87    }
88
89    fn on_draw_line(&mut self, _server_pen: Option<&dyn IPen>, client_pen: Option<&dyn IPen>, p1: Point, p2: Point) {
90        if !self.live {
91            return;
92        }
93        if let Some(point) = self.current_point {
94            if hit_test_line(client_pen, p1, p2, point) {
95                self.hit();
96                return;
97            }
98        }
99        if let Some(geometry) = self.current_geometry.clone() {
100            self.hit_with(hit_test_line_geometry(client_pen, p1, p2, &geometry));
101        }
102    }
103
104    fn on_draw_rectangle(
105        &mut self,
106        server_brush: Option<&dyn IBrush>,
107        _server_pen: Option<&dyn IPen>,
108        client_pen: Option<&dyn IPen>,
109        rect: RoundedRect,
110        _box_shadows: &BoxShadows,
111    ) {
112        if !self.live {
113            return;
114        }
115        if let Some(point) = self.current_point {
116            if hit_test_rectangle(server_brush, client_pen, rect, point) {
117                self.hit();
118                return;
119            }
120        }
121        if let Some(geometry) = self.current_geometry.clone() {
122            self.hit_with(hit_test_rectangle_geometry(server_brush, client_pen, rect, &geometry));
123        }
124    }
125
126    fn on_draw_ellipse(
127        &mut self,
128        server_brush: Option<&dyn IBrush>,
129        _server_pen: Option<&dyn IPen>,
130        client_pen: Option<&dyn IPen>,
131        rect: Rect,
132    ) {
133        if !self.live {
134            return;
135        }
136        if let Some(point) = self.current_point {
137            if hit_test_ellipse(server_brush, client_pen, rect, point) {
138                self.hit();
139                return;
140            }
141        }
142        if let Some(geometry) = self.current_geometry.clone() {
143            self.hit_with(hit_test_ellipse_geometry(server_brush, client_pen, rect, &geometry));
144        }
145    }
146
147    fn on_draw_geometry(
148        &mut self,
149        server_brush: Option<&dyn IBrush>,
150        _server_pen: Option<&dyn IPen>,
151        client_pen: Option<&dyn IPen>,
152        geometry: Option<&Arc<dyn IGeometryImpl>>,
153    ) {
154        let Some(geometry) = geometry else { return };
155        if !self.live {
156            return;
157        }
158        if let Some(point) = self.current_point {
159            if (server_brush.is_some() && geometry.fill_contains(point))
160                || (client_pen.is_some() && geometry.stroke_contains(client_pen, point))
161            {
162                self.hit();
163                return;
164            }
165        }
166        if let Some(current_geometry_impl) = self.current_geometry.as_ref().and_then(|g| g.platform_impl()) {
167            let result = hit_test_geometry(server_brush, client_pen, Some(&current_geometry_impl), Some(geometry));
168            if (result as i32) > (IntersectionResult::Empty as i32) {
169                self.hit_with(result);
170            }
171        }
172    }
173
174    fn on_draw_glyph_run(&mut self, _server_brush: Option<&dyn IBrush>, glyph_run: Option<&std::sync::Arc<dyn IGlyphRunImpl>>) {
175        let Some(glyph_run) = glyph_run else { return };
176        if !self.live {
177            return;
178        }
179        if let Some(point) = self.current_point {
180            if glyph_run.bounds().contains_exclusive(point) {
181                self.hit();
182                return;
183            }
184        }
185        if let Some(geometry) = &self.current_geometry {
186            let result = Self::get_intersection_detail(glyph_run.bounds(), geometry.bounds());
187            self.hit_with(result);
188        }
189    }
190
191    fn on_draw_bitmap(&mut self, _bitmap: Option<&std::sync::Arc<crate::platform::SharedBitmapImpl>>, _opacity: f64, _source_rect: Rect, dest_rect: Rect) {
192        if !self.live {
193            return;
194        }
195        if let Some(point) = self.current_point {
196            if dest_rect.contains(point) {
197                self.hit();
198                return;
199            }
200        }
201        if let Some(geometry) = &self.current_geometry {
202            let result = Self::get_intersection_detail(dest_rect, geometry.bounds());
203            self.hit_with(result);
204        }
205    }
206
207    fn on_draw_custom(&mut self, operation: Option<&std::sync::Arc<dyn ICustomDrawOperation>>) {
208        if !self.live {
209            return;
210        }
211        let Some(operation) = operation else { return };
212        if let Some(point) = self.current_point {
213            if operation.hit_test(point) {
214                self.hit();
215                return;
216            }
217        }
218        if let Some(geometry) = self.current_geometry.clone() {
219            self.hit_with(operation.hit_test_geometry(&geometry));
220        }
221    }
222
223    fn on_push_clip(&mut self, clip: RoundedRect) -> HitTestScope {
224        let scope = HitTestScope::new(self.live);
225        if self.live
226            && (self.current_point.is_some_and(|point| !clip.rect.contains(point))
227                || self.current_geometry.as_ref().is_some_and(|geometry| !clip.rect.contains_rect(geometry.bounds())))
228        {
229            self.live = false;
230        }
231        scope
232    }
233
234    fn on_push_geometry_clip(&mut self, geometry: Option<&Arc<dyn IGeometryImpl>>) -> HitTestScope {
235        let scope = HitTestScope::new(self.live);
236        if let (true, Some(geometry)) = (self.live, geometry) {
237            if self.current_point.is_some_and(|point| !geometry.fill_contains(point))
238                || self.current_geometry.as_ref().and_then(|g| g.platform_impl()).is_some_and(|current| {
239                    current.get_fill_intersection_result(&**geometry) == IntersectionResult::Empty
240                })
241            {
242                self.live = false;
243            }
244        }
245        scope
246    }
247
248    fn on_push_opacity(&mut self, _opacity: f64) -> HitTestScope {
249        HitTestScope::new(self.live)
250    }
251
252    fn on_push_opacity_mask(&mut self, _brush: Option<&dyn IBrush>, _bounds: Rect) -> HitTestScope {
253        HitTestScope::new(self.live)
254    }
255
256    fn on_push_transform(&mut self, matrix: Matrix) -> HitTestScope {
257        let mut scope = HitTestScope::new(self.live);
258        if self.live {
259            match matrix.try_invert() {
260                Some(inverted) => {
261                    scope.restore_point = true;
262                    if let Some(point) = self.current_point {
263                        scope.saved_point = Some(point);
264                        self.current_point = Some(inverted.transform(point));
265                    } else if let Some(current) = self.current_geometry.take() {
266                        if let Some(saved) = self.saved_geometries.as_mut() {
267                            saved.push(current.clone());
268                        }
269                        let clone = current.clone_geometry();
270                        let existing = clone.transform().map_or(Matrix::IDENTITY, |t| t.value());
271                        clone.set_transform(MatrixTransform::with_matrix(existing * inverted));
272                        self.current_geometry = Some(clone);
273                    }
274                }
275                None => self.live = false,
276            }
277        }
278        scope
279    }
280
281    fn on_push_render_options(&mut self, _options: RenderOptions) -> HitTestScope {
282        HitTestScope::new(self.live)
283    }
284
285    fn on_push_text_options(&mut self, _options: TextOptions) -> HitTestScope {
286        HitTestScope::new(self.live)
287    }
288
289    fn on_push_effect(&mut self, _effect: Option<&std::sync::Arc<dyn crate::media::IImmutableEffect>>, _bounds: Rect) -> HitTestScope {
290        HitTestScope::new(self.live)
291    }
292
293    fn on_pop(&mut self, scope: HitTestScope) {
294        self.live = scope.saved_live;
295        if scope.restore_point {
296            self.current_point = scope.saved_point;
297            self.current_geometry = self.saved_geometries.as_mut().and_then(Vec::pop);
298        }
299    }
300}
301
302impl RenderDataStream {
303    /// Whether anything recorded is hit at a point.
304    pub fn hit_test(&self, point: Point) -> bool {
305        let mut visitor = HitTestVisitor::for_point(point);
306        self.visit(&mut visitor);
307        visitor.hit_found
308    }
309
310    /// How the recorded content intersects a geometry.
311    pub fn hit_test_geometry(&self, geometry: &Ref<Geometry>) -> IntersectionResult {
312        let mut visitor = HitTestVisitor::for_geometry(geometry.clone());
313        self.visit(&mut visitor);
314        visitor.hit_result
315    }
316}
317
318fn hit_test_line(client_pen: Option<&dyn IPen>, p1: Point, p2: Point, p: Point) -> bool {
319    let Some(client_pen) = client_pen else { return false };
320
321    let half_thickness = client_pen.thickness() / 2.0;
322    let min_x = p1.x.min(p2.x) - half_thickness;
323    let max_x = p1.x.max(p2.x) + half_thickness;
324    let min_y = p1.y.min(p2.y) - half_thickness;
325    let max_y = p1.y.max(p2.y) + half_thickness;
326
327    if p.x < min_x || p.x > max_x || p.y < min_y || p.y > max_y {
328        return false;
329    }
330
331    let length = |x: f64, y: f64| (x * x + y * y).sqrt();
332
333    let (ap_x, ap_y) = (p.x - p1.x, p.y - p1.y);
334    let dot1 = (p2.x - p1.x) * ap_x + (p2.y - p1.y) * ap_y;
335    if dot1 < 0.0 {
336        return length(ap_x, ap_y) <= half_thickness;
337    }
338
339    let (bp_x, bp_y) = (p.x - p2.x, p.y - p2.y);
340    let dot2 = (p1.x - p2.x) * bp_x + (p1.y - p2.y) * bp_y;
341    if dot2 < 0.0 {
342        return length(bp_x, bp_y) <= half_thickness;
343    }
344
345    let b_xa_x = p2.x - p1.x;
346    let b_ya_y = p2.y - p1.y;
347
348    let distance = (b_xa_x * (p.y - p1.y) - b_ya_y * (p.x - p1.x)) / (b_xa_x * b_xa_x + b_ya_y * b_ya_y).sqrt();
349
350    distance.abs() <= half_thickness
351}
352
353fn hit_test_line_geometry(client_pen: Option<&dyn IPen>, p1: Point, p2: Point, geometry: &Ref<Geometry>) -> IntersectionResult {
354    if client_pen.is_none() {
355        return IntersectionResult::NotCalculated;
356    }
357    hit_test_geometry(
358        None,
359        client_pen,
360        geometry.platform_impl().as_ref(),
361        LineGeometry::with_points(p1, p2).platform_impl().as_ref(),
362    )
363}
364
365fn hit_test_rectangle(server_brush: Option<&dyn IBrush>, client_pen: Option<&dyn IPen>, rect: RoundedRect, p: Point) -> bool {
366    let stroke_thickness_adjustment = client_pen.map_or(0.0, |pen| pen.thickness() / 2.0);
367
368    if rect.is_rounded() {
369        let outer = rect.inflate(stroke_thickness_adjustment, stroke_thickness_adjustment);
370        if outer.contains_exclusive(p) {
371            if server_brush.is_some() {
372                return true;
373            }
374            let inner = rect.deflate(stroke_thickness_adjustment, stroke_thickness_adjustment);
375            return !inner.contains_exclusive(p);
376        }
377    } else {
378        let outer = rect.rect.inflate(stroke_thickness_adjustment);
379        if outer.contains_exclusive(p) {
380            if server_brush.is_some() {
381                return true;
382            }
383            let inner = rect.rect.deflate(stroke_thickness_adjustment);
384            return !inner.contains_exclusive(p);
385        }
386    }
387
388    false
389}
390
391fn hit_test_rectangle_geometry(
392    server_brush: Option<&dyn IBrush>,
393    client_pen: Option<&dyn IPen>,
394    rect: RoundedRect,
395    geometry: &Ref<Geometry>,
396) -> IntersectionResult {
397    let target = if rect.is_rounded() {
398        RectangleGeometry::with_rect_and_radii(rect.rect, rect.radii_top_left.x, rect.radii_top_left.y)
399    } else {
400        RectangleGeometry::with_rect(rect.rect)
401    };
402    hit_test_geometry(server_brush, client_pen, geometry.platform_impl().as_ref(), target.platform_impl().as_ref())
403}
404
405fn hit_test_geometry(
406    server_brush: Option<&dyn IBrush>,
407    client_pen: Option<&dyn IPen>,
408    current_geometry: Option<&Arc<dyn IGeometryImpl>>,
409    target_geometry: Option<&Arc<dyn IGeometryImpl>>,
410) -> IntersectionResult {
411    let (Some(current_geometry), Some(target_geometry)) = (current_geometry, target_geometry) else {
412        return IntersectionResult::Empty;
413    };
414
415    match (server_brush, client_pen) {
416        (None, Some(client_pen)) => {
417            current_geometry.get_fill_intersection_result(&*target_geometry.get_widened_geometry(client_pen))
418        }
419        (Some(_), Some(client_pen)) if client_pen.thickness() > 0.0 => {
420            let stroke_geometry = target_geometry.get_widened_geometry(client_pen);
421            let combined = CombinedGeometry::with_geometries(
422                ImmutableGeometry::new(Some(stroke_geometry)).upcast(),
423                ImmutableGeometry::new(Some(target_geometry.clone())).upcast(),
424            );
425            match combined.platform_impl() {
426                Some(combined_impl) => current_geometry.get_fill_intersection_result(&*combined_impl),
427                None => IntersectionResult::Empty,
428            }
429        }
430        (Some(_), _) => current_geometry.get_fill_intersection_result(&**target_geometry),
431        (None, None) => IntersectionResult::Empty,
432    }
433}
434
435fn hit_test_ellipse(server_brush: Option<&dyn IBrush>, client_pen: Option<&dyn IPen>, rect: Rect, p: Point) -> bool {
436    let center = rect.center();
437
438    let stroke_thickness = client_pen.map_or(0.0, |pen| pen.thickness());
439
440    let mut rx = rect.width / 2.0 + stroke_thickness / 2.0;
441    let mut ry = rect.height / 2.0 + stroke_thickness / 2.0;
442
443    let dx = p.x - center.x;
444    let dy = p.y - center.y;
445
446    if dx.abs() > rx || dy.abs() > ry {
447        return false;
448    }
449
450    if server_brush.is_some() {
451        return ellipse_contains(dx, dy, rx, ry);
452    }
453
454    if stroke_thickness > 0.0 {
455        let in_stroke = ellipse_contains(dx, dy, rx, ry);
456
457        rx = rect.width / 2.0 - stroke_thickness / 2.0;
458        ry = rect.height / 2.0 - stroke_thickness / 2.0;
459
460        let in_inner = ellipse_contains(dx, dy, rx, ry);
461
462        return in_stroke && !in_inner;
463    }
464
465    false
466}
467
468fn hit_test_ellipse_geometry(
469    server_brush: Option<&dyn IBrush>,
470    client_pen: Option<&dyn IPen>,
471    rect: Rect,
472    geometry: &Ref<Geometry>,
473) -> IntersectionResult {
474    hit_test_geometry(
475        server_brush,
476        client_pen,
477        geometry.platform_impl().as_ref(),
478        EllipseGeometry::with_rect(rect).platform_impl().as_ref(),
479    )
480}
481
482fn ellipse_contains(dx: f64, dy: f64, radius_x: f64, radius_y: f64) -> bool {
483    let rx2 = radius_x * radius_x;
484    let ry2 = radius_y * radius_y;
485
486    let distance = ry2 * dx * dx + rx2 * dy * dy;
487
488    distance <= rx2 * ry2
489}