// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/foundation.dart'; import 'package:vector_math/vector_math_64.dart'; import 'events.dart'; /// An object that can hit-test pointers. abstract class HitTestable { // This class is intended to be used as an interface, and should not be // extended directly; this constructor prevents instantiation and extension. HitTestable._(); /// Check whether the given position hits this object. /// /// If this given position hits this object, consider adding a [HitTestEntry] /// to the given hit test result. void hitTest(HitTestResult result, Offset position); } /// An object that can dispatch events. abstract class HitTestDispatcher { // This class is intended to be used as an interface, and should not be // extended directly; this constructor prevents instantiation and extension. HitTestDispatcher._(); /// Override this method to dispatch events. void dispatchEvent(PointerEvent event, HitTestResult result); } /// An object that can handle events. abstract class HitTestTarget { // This class is intended to be used as an interface, and should not be // extended directly; this constructor prevents instantiation and extension. HitTestTarget._(); /// Override this method to receive events. void handleEvent(PointerEvent event, HitTestEntry entry); } /// Data collected during a hit test about a specific [HitTestTarget]. /// /// Subclass this object to pass additional information from the hit test phase /// to the event propagation phase. class HitTestEntry { /// Creates a hit test entry. HitTestEntry(this.target); /// The [HitTestTarget] encountered during the hit test. final HitTestTarget target; @override String toString() => '${describeIdentity(this)}($target)'; /// Returns a matrix describing how [PointerEvent]s delivered to this /// [HitTestEntry] should be transformed from the global coordinate space of /// the screen to the local coordinate space of [target]. /// /// See also: /// /// * [BoxHitTestResult.addWithPaintTransform], which is used during hit testing /// to build up this transform. Matrix4? get transform => _transform; Matrix4? _transform; } // A type of data that can be applied to a matrix by left-multiplication. @immutable abstract class _TransformPart { const _TransformPart(); // Apply this transform part to `rhs` from the left. // // This should work as if this transform part is first converted to a matrix // and then left-multiplied to `rhs`. // // For example, if this transform part is a vector `v1`, whose corresponding // matrix is `m1 = Matrix4.translation(v1)`, then the result of // `_VectorTransformPart(v1).multiply(rhs)` should equal to `m1 * rhs`. Matrix4 multiply(Matrix4 rhs); } class _MatrixTransformPart extends _TransformPart { const _MatrixTransformPart(this.matrix); final Matrix4 matrix; @override Matrix4 multiply(Matrix4 rhs) { return matrix * rhs as Matrix4; } } class _OffsetTransformPart extends _TransformPart { const _OffsetTransformPart(this.offset); final Offset offset; @override Matrix4 multiply(Matrix4 rhs) { return rhs.clone()..leftTranslate(offset.dx, offset.dy); } } /// The result of performing a hit test. class HitTestResult { /// Creates an empty hit test result. HitTestResult() : _path = <HitTestEntry>[], _transforms = <Matrix4>[Matrix4.identity()], _localTransforms = <_TransformPart>[]; /// Wraps `result` (usually a subtype of [HitTestResult]) to create a /// generic [HitTestResult]. /// /// The [HitTestEntry]s added to the returned [HitTestResult] are also /// added to the wrapped `result` (both share the same underlying data /// structure to store [HitTestEntry]s). HitTestResult.wrap(HitTestResult result) : _path = result._path, _transforms = result._transforms, _localTransforms = result._localTransforms; /// An unmodifiable list of [HitTestEntry] objects recorded during the hit test. /// /// The first entry in the path is the most specific, typically the one at /// the leaf of tree being hit tested. Event propagation starts with the most /// specific (i.e., first) entry and proceeds in order through the path. Iterable<HitTestEntry> get path => _path; final List<HitTestEntry> _path; // A stack of transform parts. // // The transform part stack leading from global to the current object is stored // in 2 parts: // // * `_transforms` are globalized matrices, meaning they have been multiplied // by the ancestors and are thus relative to the global coordinate space. // * `_localTransforms` are local transform parts, which are relative to the // parent's coordinate space. // // When new transform parts are added they're appended to `_localTransforms`, // and are converted to global ones and moved to `_transforms` only when used. final List<Matrix4> _transforms; final List<_TransformPart> _localTransforms; // Globalize all transform parts in `_localTransforms` and move them to // _transforms. void _globalizeTransforms() { if (_localTransforms.isEmpty) { return; } Matrix4 last = _transforms.last; for (final _TransformPart part in _localTransforms) { last = part.multiply(last); _transforms.add(last); } _localTransforms.clear(); } Matrix4 get _lastTransform { _globalizeTransforms(); assert(_localTransforms.isEmpty); return _transforms.last; } /// Add a [HitTestEntry] to the path. /// /// The new entry is added at the end of the path, which means entries should /// be added in order from most specific to least specific, typically during an /// upward walk of the tree being hit tested. void add(HitTestEntry entry) { assert(entry._transform == null); entry._transform = _lastTransform; _path.add(entry); } /// Pushes a new transform matrix that is to be applied to all future /// [HitTestEntry]s added via [add] until it is removed via [popTransform]. /// /// This method is only to be used by subclasses, which must provide /// coordinate space specific public wrappers around this function for their /// users (see [BoxHitTestResult.addWithPaintTransform] for such an example). /// /// The provided `transform` matrix should describe how to transform /// [PointerEvent]s from the coordinate space of the method caller to the /// coordinate space of its children. In most cases `transform` is derived /// from running the inverted result of [RenderObject.applyPaintTransform] /// through [PointerEvent.removePerspectiveTransform] to remove /// the perspective component. /// /// If the provided `transform` is a translation matrix, it is much faster /// to use [pushOffset] with the translation offset instead. /// /// [HitTestable]s need to call this method indirectly through a convenience /// method defined on a subclass before hit testing a child that does not /// have the same origin as the parent. After hit testing the child, /// [popTransform] has to be called to remove the child-specific `transform`. /// /// See also: /// /// * [pushOffset], which is similar to [pushTransform] but is limited to /// translations, and is faster in such cases. /// * [BoxHitTestResult.addWithPaintTransform], which is a public wrapper /// around this function for hit testing on [RenderBox]s. @protected void pushTransform(Matrix4 transform) { assert(transform != null); assert( _debugVectorMoreOrLessEquals(transform.getRow(2), Vector4(0, 0, 1, 0)) && _debugVectorMoreOrLessEquals(transform.getColumn(2), Vector4(0, 0, 1, 0)), 'The third row and third column of a transform matrix for pointer ' 'events must be Vector4(0, 0, 1, 0) to ensure that a transformed ' 'point is directly under the pointing device. Did you forget to run the paint ' 'matrix through PointerEvent.removePerspectiveTransform? ' 'The provided matrix is:\n$transform' ); _localTransforms.add(_MatrixTransformPart(transform)); } /// Pushes a new translation offset that is to be applied to all future /// [HitTestEntry]s added via [add] until it is removed via [popTransform]. /// /// This method is only to be used by subclasses, which must provide /// coordinate space specific public wrappers around this function for their /// users (see [BoxHitTestResult.addWithPaintOffset] for such an example). /// /// The provided `offset` should describe how to transform [PointerEvent]s from /// the coordinate space of the method caller to the coordinate space of its /// children. Usually `offset` is the inverse of the offset of the child /// relative to the parent. /// /// [HitTestable]s need to call this method indirectly through a convenience /// method defined on a subclass before hit testing a child that does not /// have the same origin as the parent. After hit testing the child, /// [popTransform] has to be called to remove the child-specific `transform`. /// /// See also: /// /// * [pushTransform], which is similar to [pushOffset] but allows general /// transform besides translation. /// * [BoxHitTestResult.addWithPaintOffset], which is a public wrapper /// around this function for hit testing on [RenderBox]s. /// * [SliverHitTestResult.addWithAxisOffset], which is a public wrapper /// around this function for hit testing on [RenderSliver]s. @protected void pushOffset(Offset offset) { assert(offset != null); _localTransforms.add(_OffsetTransformPart(offset)); } /// Removes the last transform added via [pushTransform] or [pushOffset]. /// /// This method is only to be used by subclasses, which must provide /// coordinate space specific public wrappers around this function for their /// users (see [BoxHitTestResult.addWithPaintTransform] for such an example). /// /// This method must be called after hit testing is done on a child that /// required a call to [pushTransform] or [pushOffset]. /// /// See also: /// /// * [pushTransform] and [pushOffset], which describes the use case of this /// function pair in more details. @protected void popTransform() { if (_localTransforms.isNotEmpty) _localTransforms.removeLast(); else _transforms.removeLast(); assert(_transforms.isNotEmpty); } bool _debugVectorMoreOrLessEquals(Vector4 a, Vector4 b, { double epsilon = precisionErrorTolerance }) { bool result = true; assert(() { final Vector4 difference = a - b; result = difference.storage.every((double component) => component.abs() < epsilon); return true; }()); return result; } @override String toString() => 'HitTestResult(${_path.isEmpty ? "<empty path>" : _path.join(", ")})'; }