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