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