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