Skip to main content

inset_gestures/
hit_test.rs

1//! Flutter counterpart: `gestures/hit_test.dart`.
2
3use std::any::Any;
4use std::fmt::{self, Debug};
5use std::rc::Rc;
6
7use inset_embedder::{Matrix4, Offset, ViewId};
8use inset_foundation::{App, HandleId, RetainedHandleId};
9
10use crate::events::PointerEvent;
11
12/// How far an entry of [`HitTestResult::push_transform`]'s third row or column may be from
13/// `(0, 0, 1, 0)`.
14///
15/// Dart checks against `precisionErrorTolerance` (1e-10), which suits vector_math's f64
16/// matrices. [`Matrix4`] is f32, and a single f32 inverse already leaves the diagonal entry
17/// one ulp below 1 (6e-8). Eight f32 epsilons (about 1e-6) is sixteen times the worst a
18/// million inverted scale, rotation, skew and translation chains produced, and still catches
19/// any perspective a transform could carry.
20const MATRIX_PRECISION_ERROR_TOLERANCE: f64 = 8.0 * f32::EPSILON as f64;
21
22/// An object that can hit-test pointers.
23pub trait HitTestable {
24    /// Fills the provided [`HitTestResult`] with [`HitTestEntry`]s for objects that
25    /// are hit at the given `position` in the view identified by `view_id`.
26    fn hit_test_in_view(
27        &self,
28        app: &mut App,
29        result: &mut HitTestResult,
30        position: Offset,
31        view_id: ViewId,
32    );
33}
34
35/// An object that can dispatch events.
36pub trait HitTestDispatcher {
37    /// Override this method to dispatch events.
38    fn dispatch_event(&self, app: &mut App, event: PointerEvent, result: &HitTestResult);
39}
40
41/// An object that can handle events.
42pub trait HitTestTarget: Debug + Any {
43    /// Override this method to receive events.
44    fn handle_event(&self, app: &mut App, event: &PointerEvent, entry: &HitTestEntry);
45
46    /// The arena object this target delivers to, if it is one, so a result kept across turns
47    /// can [`retain`](App::retain) it: Dart keeps the object alive by holding it in the path.
48    fn retained_handle(&self) -> Option<HandleId> {
49        None
50    }
51}
52
53/// Data collected during a hit test about a specific [`HitTestTarget`].
54///
55/// Subclass this object to pass additional information from the hit test phase
56/// to the event propagation phase.
57pub struct HitTestEntry {
58    target: Rc<dyn HitTestTarget>,
59    transform: Option<Matrix4>,
60}
61
62impl HitTestEntry {
63    /// Creates a hit test entry.
64    pub fn new(target: impl HitTestTarget + 'static) -> HitTestEntry {
65        HitTestEntry {
66            target: Rc::new(target),
67            transform: None,
68        }
69    }
70
71    /// The [`HitTestTarget`] encountered during the hit test.
72    pub fn target(&self) -> &dyn HitTestTarget {
73        &*self.target
74    }
75
76    /// The [`target`](Self::target) as the `Rc` that holds it, to keep it past the hit test.
77    pub fn shared_target(&self) -> &Rc<dyn HitTestTarget> {
78        &self.target
79    }
80
81    /// Returns a matrix describing how [`PointerEvent`]s delivered to this
82    /// [`HitTestEntry`] should be transformed from the global coordinate space of
83    /// the screen to the local coordinate space of [`target`](Self::target).
84    ///
85    /// See also:
86    ///
87    ///  * `BoxHitTestResult.addWithPaintTransform`, which is used during hit testing
88    ///    to build up this transform.
89    pub fn transform(&self) -> Option<Matrix4> {
90        self.transform
91    }
92}
93
94/// An entry for a target that others hold too, such as a span of a paragraph's text.
95impl From<Rc<dyn HitTestTarget>> for HitTestEntry {
96    fn from(target: Rc<dyn HitTestTarget>) -> HitTestEntry {
97        HitTestEntry {
98            target,
99            transform: None,
100        }
101    }
102}
103
104impl Debug for HitTestEntry {
105    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106        write!(f, "HitTestEntry({:?})", self.target)
107    }
108}
109
110/// A type of data that can be applied to a matrix by left-multiplication.
111enum TransformPart {
112    Matrix(Matrix4),
113    Offset(Offset),
114}
115
116impl TransformPart {
117    /// Apply this transform part to `rhs` from the left.
118    ///
119    /// This should work as if this transform part is first converted to a matrix
120    /// and then left-multiplied to `rhs`.
121    fn multiply(&self, rhs: Matrix4) -> Matrix4 {
122        match *self {
123            TransformPart::Matrix(matrix) => matrix.then(&rhs),
124            TransformPart::Offset(offset) => {
125                Matrix4::translation(offset.dx() as f32, offset.dy() as f32).then(&rhs)
126            }
127        }
128    }
129}
130
131/// The result of performing a hit test.
132///
133/// Dart's `HitTestResult.wrap` shares one result between a base and a subtype
134/// view; here that view is `&mut HitTestResult` (or a newtype over it, such as
135/// `BoxHitTestResult`).
136pub struct HitTestResult {
137    path: Vec<HitTestEntry>,
138    /// The retained handles that keep the path's targets while the result outlives its turn;
139    /// empty until [`retain_targets`](Self::retain_targets).
140    retained: Vec<RetainedHandleId>,
141    // A stack of transform parts.
142    //
143    // The transform part stack leading from global to the current object is stored
144    // in 2 parts:
145    //
146    //  * `transforms` are globalized matrices, meaning they have been multiplied
147    //    by the ancestors and are thus relative to the global coordinate space.
148    //  * `local_transforms` are local transform parts, which are relative to the
149    //    parent's coordinate space.
150    //
151    // When new transform parts are added they're appended to `local_transforms`,
152    // and are converted to global ones and moved to `transforms` only when used.
153    transforms: Vec<Matrix4>,
154    local_transforms: Vec<TransformPart>,
155}
156
157impl HitTestResult {
158    /// Creates an empty hit test result.
159    pub fn new() -> HitTestResult {
160        HitTestResult {
161            path: Vec::new(),
162            retained: Vec::new(),
163            transforms: vec![Matrix4::IDENTITY],
164            local_transforms: Vec::new(),
165        }
166    }
167
168    /// Retains every object on the path, for a result kept past this turn (Dart keeps them alive
169    /// by holding the path). The retained handles live in the result: dropping it releases them
170    /// at the next checkpoint, [`release_targets`](Self::release_targets) releases them now.
171    pub fn retain_targets(&mut self, app: &mut App) {
172        if !self.retained.is_empty() {
173            return;
174        }
175        self.retained = self
176            .path
177            .iter()
178            .filter_map(|entry| entry.target.retained_handle())
179            .map(|id| app.retain_id(id))
180            .collect();
181    }
182
183    /// Releases what [`retain_targets`](Self::retain_targets) retained, without waiting for the
184    /// checkpoint.
185    pub fn release_targets(&mut self, app: &mut App) {
186        for handle in self.retained.drain(..) {
187            app.release_id(handle);
188        }
189    }
190
191    /// An unmodifiable list of [`HitTestEntry`] objects recorded during the hit test.
192    ///
193    /// The first entry in the path is the most specific, typically the one at
194    /// the leaf of tree being hit tested. Event propagation starts with the most
195    /// specific (i.e., first) entry and proceeds in order through the path.
196    pub fn path(&self) -> &[HitTestEntry] {
197        &self.path
198    }
199
200    // Globalize all transform parts in `local_transforms` and move them to
201    // `transforms`.
202    fn globalize_transforms(&mut self) {
203        if self.local_transforms.is_empty() {
204            return;
205        }
206        let mut last = *self.transforms.last().unwrap();
207        for part in self.local_transforms.drain(..) {
208            last = part.multiply(last);
209            self.transforms.push(last);
210        }
211    }
212
213    fn last_transform(&mut self) -> Matrix4 {
214        self.globalize_transforms();
215        debug_assert!(self.local_transforms.is_empty());
216        *self.transforms.last().unwrap()
217    }
218
219    /// Add a [`HitTestEntry`] to the path.
220    ///
221    /// The new entry is added at the end of the path, which means entries should
222    /// be added in order from most specific to least specific, typically during an
223    /// upward walk of the tree being hit tested.
224    pub fn add(&mut self, mut entry: HitTestEntry) {
225        debug_assert!(entry.transform.is_none());
226        entry.transform = Some(self.last_transform());
227        self.path.push(entry);
228    }
229
230    /// Pushes a new transform matrix that is to be applied to all future
231    /// [`HitTestEntry`]s added via [`add`](Self::add) until it is removed via [`pop_transform`](Self::pop_transform).
232    ///
233    /// This method is only to be used by subclasses, which must provide
234    /// coordinate space specific public wrappers around this function for their
235    /// users (see `BoxHitTestResult.addWithPaintTransform` for such an example).
236    ///
237    /// The provided `transform` matrix should describe how to transform
238    /// [`PointerEvent`]s from the coordinate space of the method caller to the
239    /// coordinate space of its children. In most cases `transform` is derived
240    /// from running the inverted result of `RenderObject.applyPaintTransform`
241    /// through [`remove_perspective_transform`](crate::remove_perspective_transform) to remove
242    /// the perspective component.
243    ///
244    /// If the provided `transform` is a translation matrix, it is much faster
245    /// to use [`push_offset`](Self::push_offset) with the translation offset instead.
246    ///
247    /// [`HitTestable`]s need to call this method indirectly through a convenience
248    /// method defined on a subclass before hit testing a child that does not
249    /// have the same origin as the parent. After hit testing the child,
250    /// [`pop_transform`](Self::pop_transform) has to be called to remove the child-specific `transform`.
251    ///
252    /// See also:
253    ///
254    ///  * [`push_offset`](Self::push_offset), which is similar to [`push_transform`](Self::push_transform) but is limited to
255    ///    translations, and is faster in such cases.
256    ///  * `BoxHitTestResult.addWithPaintTransform`, which is a public wrapper
257    ///    around this function for hit testing on `RenderBox`s.
258    pub fn push_transform(&mut self, transform: Matrix4) {
259        debug_assert!(
260            {
261                let values = transform.to_flutter_array();
262                let row2 = [values[2], values[6], values[10], values[14]];
263                let col2 = [values[8], values[9], values[10], values[11]];
264                debug_vector_more_or_less_equals(
265                    row2,
266                    [0.0, 0.0, 1.0, 0.0],
267                    MATRIX_PRECISION_ERROR_TOLERANCE,
268                ) && debug_vector_more_or_less_equals(
269                    col2,
270                    [0.0, 0.0, 1.0, 0.0],
271                    MATRIX_PRECISION_ERROR_TOLERANCE,
272                )
273            },
274            "The third row and third column of a transform matrix for pointer \
275             events must be Vector4(0, 0, 1, 0) to ensure that a transformed \
276             point is directly under the pointing device. Did you forget to run the paint \
277             matrix through remove_perspective_transform? \
278             The provided matrix is:\n{transform:?}"
279        );
280        self.local_transforms.push(TransformPart::Matrix(transform));
281    }
282
283    /// Pushes a new translation offset that is to be applied to all future
284    /// [`HitTestEntry`]s added via [`add`](Self::add) until it is removed via [`pop_transform`](Self::pop_transform).
285    ///
286    /// This method is only to be used by subclasses, which must provide
287    /// coordinate space specific public wrappers around this function for their
288    /// users (see `BoxHitTestResult.addWithPaintOffset` for such an example).
289    ///
290    /// The provided `offset` should describe how to transform [`PointerEvent`]s from
291    /// the coordinate space of the method caller to the coordinate space of its
292    /// children. Usually `offset` is the inverse of the offset of the child
293    /// relative to the parent.
294    ///
295    /// [`HitTestable`]s need to call this method indirectly through a convenience
296    /// method defined on a subclass before hit testing a child that does not
297    /// have the same origin as the parent. After hit testing the child,
298    /// [`pop_transform`](Self::pop_transform) has to be called to remove the child-specific `transform`.
299    ///
300    /// See also:
301    ///
302    ///  * [`push_transform`](Self::push_transform), which is similar to [`push_offset`](Self::push_offset) but allows general
303    ///    transform besides translation.
304    ///  * `BoxHitTestResult.addWithPaintOffset`, which is a public wrapper
305    ///    around this function for hit testing on `RenderBox`s.
306    ///  * `SliverHitTestResult.addWithAxisOffset`, which is a public wrapper
307    ///    around this function for hit testing on `RenderSliver`s.
308    pub fn push_offset(&mut self, offset: Offset) {
309        self.local_transforms.push(TransformPart::Offset(offset));
310    }
311
312    /// Removes the last transform added via [`push_transform`](Self::push_transform) or [`push_offset`](Self::push_offset).
313    ///
314    /// This method is only to be used by subclasses, which must provide
315    /// coordinate space specific public wrappers around this function for their
316    /// users (see `BoxHitTestResult.addWithPaintTransform` for such an example).
317    ///
318    /// This method must be called after hit testing is done on a child that
319    /// required a call to [`push_transform`](Self::push_transform) or [`push_offset`](Self::push_offset).
320    ///
321    /// See also:
322    ///
323    ///  * [`push_transform`](Self::push_transform) and [`push_offset`](Self::push_offset), which describes the use case of this
324    ///    function pair in more details.
325    pub fn pop_transform(&mut self) {
326        if !self.local_transforms.is_empty() {
327            self.local_transforms.pop();
328        } else {
329            self.transforms.pop();
330        }
331        debug_assert!(!self.transforms.is_empty());
332    }
333}
334
335impl Default for HitTestResult {
336    fn default() -> HitTestResult {
337        HitTestResult::new()
338    }
339}
340
341impl Debug for HitTestResult {
342    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
343        if self.path.is_empty() {
344            return write!(f, "HitTestResult(<empty path>)");
345        }
346        write!(f, "HitTestResult(")?;
347        for (i, entry) in self.path.iter().enumerate() {
348            if i > 0 {
349                write!(f, ", ")?;
350            }
351            write!(f, "{entry:?}")?;
352        }
353        write!(f, ")")
354    }
355}
356
357fn debug_vector_more_or_less_equals(a: [f32; 4], b: [f32; 4], epsilon: f64) -> bool {
358    let mut result = true;
359    if cfg!(debug_assertions) {
360        result = a
361            .iter()
362            .zip(b)
363            .all(|(component, expected)| (*component as f64 - expected as f64).abs() < epsilon);
364    }
365    result
366}
367
368#[cfg(test)]
369mod tests {
370    use super::*;
371
372    #[derive(Debug)]
373    struct DummyHitTestTarget;
374
375    impl HitTestTarget for DummyHitTestTarget {
376        fn handle_event(&self, _app: &mut App, _event: &PointerEvent, _entry: &HitTestEntry) {}
377    }
378
379    fn current_transform(result: &mut HitTestResult) -> Matrix4 {
380        result.add(HitTestEntry::new(DummyHitTestTarget));
381        result.path().last().unwrap().transform().unwrap()
382    }
383
384    #[test]
385    fn add_records_the_current_transform_on_the_entry() {
386        let transform = Matrix4::translation(40.0, 150.0);
387        let mut result = HitTestResult::new();
388        result.push_transform(transform);
389        result.add(HitTestEntry::new(DummyHitTestTarget));
390        result.add(HitTestEntry::new(DummyHitTestTarget));
391        assert_eq!(result.path().len(), 2);
392        assert!(
393            result
394                .path()
395                .iter()
396                .all(|entry| entry.transform() == Some(transform))
397        );
398    }
399
400    #[test]
401    fn push_and_pop_transforms() {
402        let mut result = HitTestResult::new();
403
404        let m1 = Matrix4::translation(10.0, 20.0);
405        let m2 = Matrix4::rotation(1.0);
406        let m3 = Matrix4::scale(1.1, 1.2);
407
408        result.push_transform(m1);
409        assert_eq!(current_transform(&mut result), m1);
410
411        result.push_transform(m2);
412        assert_eq!(current_transform(&mut result), m2.then(&m1));
413        assert_eq!(current_transform(&mut result), m2.then(&m1)); // Test repeated add
414
415        result.push_transform(m3);
416        assert_eq!(current_transform(&mut result), m3.then(&m2.then(&m1)));
417
418        result.pop_transform();
419        result.pop_transform();
420        assert_eq!(current_transform(&mut result), m1);
421
422        result.pop_transform();
423        result.push_transform(m3);
424        assert_eq!(current_transform(&mut result), m3);
425
426        result.push_transform(m2);
427        assert_eq!(current_transform(&mut result), m2.then(&m3));
428    }
429
430    #[test]
431    fn push_and_pop_offsets() {
432        let mut result = HitTestResult::new();
433
434        let m1 = Matrix4::rotation(1.0);
435        let m2 = Matrix4::scale(1.1, 1.2);
436        let o3 = Offset::new(10.0, 20.0);
437        let m3 = Matrix4::translation(o3.dx() as f32, o3.dy() as f32);
438
439        // Test pushing offset as the first element
440        result.push_offset(o3);
441        assert_eq!(current_transform(&mut result), m3);
442        result.pop_transform();
443
444        result.push_offset(o3);
445        result.push_transform(m1);
446        assert_eq!(current_transform(&mut result), m1.then(&m3));
447        assert_eq!(current_transform(&mut result), m1.then(&m3)); // Test repeated add
448
449        result.push_transform(m2);
450        assert_eq!(current_transform(&mut result), m2.then(&m1.then(&m3)));
451
452        result.pop_transform();
453        result.pop_transform();
454        result.pop_transform();
455        assert_eq!(current_transform(&mut result), Matrix4::IDENTITY);
456
457        result.push_transform(m2);
458        result.push_offset(o3);
459        result.push_transform(m1);
460        assert_eq!(current_transform(&mut result), m1.then(&m3.then(&m2)));
461
462        result.pop_transform();
463        assert_eq!(current_transform(&mut result), m3.then(&m2));
464    }
465
466    /// The inverse of the card scale the tab prototype hit-tested through. f32 rounding puts
467    /// its third-row diagonal at 0.99999994, which Dart's 1e-10 tolerance rejected.
468    #[test]
469    fn an_inverted_f32_scale_is_accepted() {
470        let inverse = Matrix4::scale(0.432_428_2, 0.432_428_2)
471            .invert()
472            .expect("a scale is invertible");
473        let values = inverse.to_flutter_array();
474        assert_eq!(values[0], 2.312_522_6);
475        assert_eq!(values[10], 0.999_999_94, "the rounding this test is about");
476
477        let mut result = HitTestResult::new();
478        result.push_transform(inverse);
479        assert_eq!(current_transform(&mut result), inverse);
480    }
481
482    #[test]
483    #[cfg(debug_assertions)]
484    #[should_panic(expected = "The third row and third column of a transform matrix")]
485    fn a_perspective_transform_is_still_rejected() {
486        let mut values = Matrix4::IDENTITY.to_flutter_array();
487        // vector_math's `setEntry(3, 2, 0.001)`, the usual perspective.
488        values[11] = 0.001;
489        HitTestResult::new().push_transform(Matrix4::from_flutter_array(&values));
490    }
491}