// Copyright 2015 The Chromium 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 'dart:collection'; 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 with the implements // keyword, and should not be extended directly. factory HitTestable._() => null; /// 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 with the implements // keyword, and should not be extended directly. factory HitTestDispatcher._() => null; /// 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 with the implements // keyword, and should not be extended directly. factory HitTestTarget._() => null; /// 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() => '$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: /// /// * [HitTestResult.addWithPaintTransform], which is used during hit testing /// to build up the transform returned by this method. Matrix4 get transform => _transform; Matrix4 _transform; } /// The result of performing a hit test. class HitTestResult { /// Creates an empty hit test result. HitTestResult() : _path = <HitTestEntry>[], _transforms = Queue<Matrix4>(); /// 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; /// 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; final Queue<Matrix4> _transforms; /// 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 = _transforms.isEmpty ? null : _transforms.last; _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. /// /// [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: /// * [BoxHitTestResult.addWithPaintTransform], 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 [RenderSlivers]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 pointer device. Did you forget to run the paint ' 'matrix through PointerEvent.removePerspectiveTransform?' 'The provided matrix is:\n$transform' ); _transforms.add(_transforms.isEmpty ? transform : transform * _transforms.last); } /// Removes the last transform added via [pushTransform]. /// /// 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]. /// /// See also: /// /// * [pushTransform], which describes the use case of this function pair in /// more details. @protected void popTransform() { assert(_transforms.isNotEmpty); _transforms.removeLast(); } 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(", ")})'; }