Unverified Commit da0a7d8b authored by Tong Mu's avatar Tong Mu Committed by GitHub

MouseTracker no longer requires annotations attached (#48453)

parent dd98046f
...@@ -30,7 +30,7 @@ typedef PointerHoverEventListener = void Function(PointerHoverEvent event); ...@@ -30,7 +30,7 @@ typedef PointerHoverEventListener = void Function(PointerHoverEvent event);
/// movements. /// movements.
/// ///
/// This is added to a layer and managed by the [MouseRegion] widget. /// This is added to a layer and managed by the [MouseRegion] widget.
class MouseTrackerAnnotation { class MouseTrackerAnnotation extends Diagnosticable {
/// Creates an annotation that can be used to find layers interested in mouse /// Creates an annotation that can be used to find layers interested in mouse
/// movements. /// movements.
const MouseTrackerAnnotation({this.onEnter, this.onHover, this.onExit}); const MouseTrackerAnnotation({this.onEnter, this.onHover, this.onExit});
...@@ -39,24 +39,13 @@ class MouseTrackerAnnotation { ...@@ -39,24 +39,13 @@ class MouseTrackerAnnotation {
/// entered the annotated region. /// entered the annotated region.
/// ///
/// This callback is triggered when the pointer has started to be contained /// This callback is triggered when the pointer has started to be contained
/// by the annotationed region for any reason. /// by the annotationed region for any reason, which means it always matches a
/// /// later [onExit].
/// More specifically, the callback is triggered by the following cases:
///
/// * A new annotated region has appeared under a pointer.
/// * An existing annotated region has moved to under a pointer.
/// * A new pointer has been added to somewhere within an annotated region.
/// * An existing pointer has moved into an annotated region.
///
/// This callback is not always matched by an [onExit]. If the render object
/// that owns the annotation is disposed while being hovered by a pointer,
/// the [onExit] callback of that annotation will never called, despite
/// the earlier call of [onEnter]. For more details, see [onExit].
/// ///
/// See also: /// See also:
/// ///
/// * [MouseRegion.onEnter], which uses this callback.
/// * [onExit], which is triggered when a mouse pointer exits the region. /// * [onExit], which is triggered when a mouse pointer exits the region.
/// * [MouseRegion.onEnter], which uses this callback.
final PointerEnterEventListener onEnter; final PointerEnterEventListener onEnter;
/// Triggered when a pointer has moved within the annotated region without /// Triggered when a pointer has moved within the annotated region without
...@@ -69,7 +58,7 @@ class MouseTrackerAnnotation { ...@@ -69,7 +58,7 @@ class MouseTrackerAnnotation {
/// * A pointer has moved onto, or moved within an annotation without buttons /// * A pointer has moved onto, or moved within an annotation without buttons
/// pressed. /// pressed.
/// ///
/// This callback is not triggered when /// This callback is not triggered when:
/// ///
/// * An annotation that is containing the pointer has moved, and still /// * An annotation that is containing the pointer has moved, and still
/// contains the pointer. /// contains the pointer.
...@@ -78,59 +67,30 @@ class MouseTrackerAnnotation { ...@@ -78,59 +67,30 @@ class MouseTrackerAnnotation {
/// Triggered when a mouse pointer, with or without buttons pressed, has /// Triggered when a mouse pointer, with or without buttons pressed, has
/// exited the annotated region when the annotated region still exists. /// exited the annotated region when the annotated region still exists.
/// ///
/// This callback is triggered when the pointer has stopped to be contained /// This callback is triggered when the pointer has stopped being contained
/// by the region, except when it's caused by the removal of the render object /// by the region for any reason, which means it always matches an earlier
/// that owns the annotation. More specifically, the callback is triggered by
/// the following cases:
///
/// * An annotated region that used to contain a pointer has moved away.
/// * A pointer that used to be within an annotated region has been removed.
/// * A pointer that used to be within an annotated region has moved away.
///
/// And is __not__ triggered by the following case,
///
/// * An annotated region that used to contain a pointer has disappeared.
///
/// The last case is the only case when [onExit] does not match an earlier
/// [onEnter]. /// [onEnter].
/// {@template flutter.mouseTracker.onExit}
/// This design is because the last case is very likely to be
/// handled improperly and cause exceptions (such as calling `setState` of the
/// disposed widget). There are a few ways to mitigate this limit:
///
/// * If the state of hovering is contained within a widget that
/// unconditionally attaches the annotation (as long as a mouse is
/// connected), then this will not be a concern, since when the annotation
/// is disposed the state is no longer used.
/// * If you're accessible to the condition that controls whether the
/// annotation is attached, then you can call the callback when that
/// condition goes from true to false.
/// * In the cases where the solutions above won't work, you can always
/// override [State.dispose] or [RenderObject.detach].
/// {@endtemplate}
///
/// Technically, whether [onExit] will be called is controlled by
/// [MouseTracker.attachAnnotation] and [MouseTracker.detachAnnotation].
/// ///
/// See also: /// See also:
/// ///
/// * [MouseRegion.onExit], which uses this callback.
/// * [onEnter], which is triggered when a mouse pointer enters the region. /// * [onEnter], which is triggered when a mouse pointer enters the region.
/// * [RenderMouseRegion.onExit], which uses this callback.
/// * [MouseRegion.onExit], which uses this callback, but is not triggered in
/// certain cases and does not always match its earier [MouseRegion.onEnter].
final PointerExitEventListener onExit; final PointerExitEventListener onExit;
@override @override
String toString() { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
final List<String> callbacks = <String>[]; super.debugFillProperties(properties);
if (onEnter != null) properties.add(FlagsSummary<Function>(
callbacks.add('enter'); 'callbacks',
if (onHover != null) <String, Function> {
callbacks.add('hover'); 'enter': onEnter,
if (onExit != null) 'hover': onHover,
callbacks.add('exit'); 'exit': onExit,
final String describeCallbacks = callbacks.isEmpty },
? '<none>' ifEmpty: '<none>',
: callbacks.join(' '); ));
return '${describeIdentity(this)}(callbacks: $describeCallbacks)';
} }
} }
...@@ -194,11 +154,9 @@ class _MouseState { ...@@ -194,11 +154,9 @@ class _MouseState {
/// ///
/// ### Details /// ### Details
/// ///
/// The state of [MouseTracker] consists of 3 parts: /// The state of [MouseTracker] consists of two parts:
/// ///
/// * The mouse devices that are connected. /// * The mouse devices that are connected.
/// * The annotations that are attached, i.e. whose owner render object is
/// painted on the screen.
/// * In which annotations each device is contained. /// * In which annotations each device is contained.
/// ///
/// The states remain stable most of the time, and are only changed at the /// The states remain stable most of the time, and are only changed at the
...@@ -247,10 +205,6 @@ class MouseTracker extends ChangeNotifier { ...@@ -247,10 +205,6 @@ class MouseTracker extends ChangeNotifier {
// mouse events from. // mouse events from.
final PointerRouter _router; final PointerRouter _router;
// The collection of annotations that are currently being tracked. It is
// operated on by [attachAnnotation] and [detachAnnotation].
final Set<MouseTrackerAnnotation> _trackedAnnotations = <MouseTrackerAnnotation>{};
// Tracks the state of connected mouse devices. // Tracks the state of connected mouse devices.
// //
// It is the source of truth for the list of connected mouse devices. // It is the source of truth for the list of connected mouse devices.
...@@ -298,7 +252,6 @@ class MouseTracker extends ChangeNotifier { ...@@ -298,7 +252,6 @@ class MouseTracker extends ChangeNotifier {
nextAnnotations: mouseState.annotations, nextAnnotations: mouseState.annotations,
previousEvent: previousEvent, previousEvent: previousEvent,
unhandledEvent: event, unhandledEvent: event,
trackedAnnotations: _trackedAnnotations,
); );
}, },
); );
...@@ -306,12 +259,12 @@ class MouseTracker extends ChangeNotifier { ...@@ -306,12 +259,12 @@ class MouseTracker extends ChangeNotifier {
// Find the annotations that is hovered by the device of the `state`. // Find the annotations that is hovered by the device of the `state`.
// //
// If the device is not connected or there are no annotations attached, empty // If the device is not connected, an empty set is returned without calling
// is returned without calling `annotationFinder`. // `annotationFinder`.
LinkedHashSet<MouseTrackerAnnotation> _findAnnotations(_MouseState state) { LinkedHashSet<MouseTrackerAnnotation> _findAnnotations(_MouseState state) {
final Offset globalPosition = state.latestEvent.position; final Offset globalPosition = state.latestEvent.position;
final int device = state.device; final int device = state.device;
return (_mouseStates.containsKey(device) && _trackedAnnotations.isNotEmpty) return (_mouseStates.containsKey(device))
? LinkedHashSet<MouseTrackerAnnotation>.from(annotationFinder(globalPosition)) ? LinkedHashSet<MouseTrackerAnnotation>.from(annotationFinder(globalPosition))
: <MouseTrackerAnnotation>{} as LinkedHashSet<MouseTrackerAnnotation>; : <MouseTrackerAnnotation>{} as LinkedHashSet<MouseTrackerAnnotation>;
} }
...@@ -332,7 +285,6 @@ class MouseTracker extends ChangeNotifier { ...@@ -332,7 +285,6 @@ class MouseTracker extends ChangeNotifier {
nextAnnotations: mouseState.annotations, nextAnnotations: mouseState.annotations,
previousEvent: mouseState.latestEvent, previousEvent: mouseState.latestEvent,
unhandledEvent: null, unhandledEvent: null,
trackedAnnotations: _trackedAnnotations,
); );
} }
); );
...@@ -428,16 +380,15 @@ class MouseTracker extends ChangeNotifier { ...@@ -428,16 +380,15 @@ class MouseTracker extends ChangeNotifier {
// null, which means the update is triggered by a new event. // null, which means the update is triggered by a new event.
// The `unhandledEvent` can be null, which means the update is not triggered // The `unhandledEvent` can be null, which means the update is not triggered
// by an event. // by an event.
// However, one of `previousEvent` or `unhandledEvent` must not be null.
static void _dispatchDeviceCallbacks({ static void _dispatchDeviceCallbacks({
@required LinkedHashSet<MouseTrackerAnnotation> lastAnnotations, @required LinkedHashSet<MouseTrackerAnnotation> lastAnnotations,
@required LinkedHashSet<MouseTrackerAnnotation> nextAnnotations, @required LinkedHashSet<MouseTrackerAnnotation> nextAnnotations,
@required PointerEvent previousEvent, @required PointerEvent previousEvent,
@required PointerEvent unhandledEvent, @required PointerEvent unhandledEvent,
@required Set<MouseTrackerAnnotation> trackedAnnotations,
}) { }) {
assert(lastAnnotations != null); assert(lastAnnotations != null);
assert(nextAnnotations != null); assert(nextAnnotations != null);
assert(trackedAnnotations != null);
final PointerEvent latestEvent = unhandledEvent ?? previousEvent; final PointerEvent latestEvent = unhandledEvent ?? previousEvent;
assert(latestEvent != null); assert(latestEvent != null);
// Order is important for mouse event callbacks. The `findAnnotations` // Order is important for mouse event callbacks. The `findAnnotations`
...@@ -446,49 +397,45 @@ class MouseTracker extends ChangeNotifier { ...@@ -446,49 +397,45 @@ class MouseTracker extends ChangeNotifier {
// The algorithm here is explained in // The algorithm here is explained in
// https://github.com/flutter/flutter/issues/41420 // https://github.com/flutter/flutter/issues/41420
// Send exit events in visual order. // Send exit events to annotations that are in last but not in next, in
final Iterable<MouseTrackerAnnotation> exitingAnnotations = // visual order.
lastAnnotations.difference(nextAnnotations); final Iterable<MouseTrackerAnnotation> exitingAnnotations = lastAnnotations.where(
(MouseTrackerAnnotation value) => !nextAnnotations.contains(value),
);
for (final MouseTrackerAnnotation annotation in exitingAnnotations) { for (final MouseTrackerAnnotation annotation in exitingAnnotations) {
final bool attached = trackedAnnotations.contains(annotation); if (annotation.onExit != null) {
// Exit is not sent if annotation is no longer attached, because this
// trigger may cause exceptions and has safer alternatives. See
// [MouseRegion.onExit] for details.
if (annotation.onExit != null && attached) {
annotation.onExit(PointerExitEvent.fromMouseEvent(latestEvent)); annotation.onExit(PointerExitEvent.fromMouseEvent(latestEvent));
} }
} }
// Send enter events in reverse visual order. // Send enter events to annotations that are not in last but in next, in
// reverse visual order.
final Iterable<MouseTrackerAnnotation> enteringAnnotations = final Iterable<MouseTrackerAnnotation> enteringAnnotations =
nextAnnotations.difference(lastAnnotations).toList().reversed; nextAnnotations.difference(lastAnnotations).toList().reversed;
for (final MouseTrackerAnnotation annotation in enteringAnnotations) { for (final MouseTrackerAnnotation annotation in enteringAnnotations) {
assert(trackedAnnotations.contains(annotation));
if (annotation.onEnter != null) { if (annotation.onEnter != null) {
annotation.onEnter(PointerEnterEvent.fromMouseEvent(latestEvent)); annotation.onEnter(PointerEnterEvent.fromMouseEvent(latestEvent));
} }
} }
// Send hover events in reverse visual order. // Send hover events to annotations that are in next, in reverse visual
// For now the order between the hover events is designed this way for no // order. The reverse visual order is chosen only because of the simplicity
// solid reasons but to keep it aligned with enter events for simplicity. // by keeping the hover events aligned with enter events.
if (unhandledEvent is PointerHoverEvent) { if (unhandledEvent is PointerHoverEvent) {
final Iterable<MouseTrackerAnnotation> hoveringAnnotations =
nextAnnotations.toList().reversed;
final Offset lastHoverPosition = previousEvent is PointerHoverEvent ? previousEvent.position : null; final Offset lastHoverPosition = previousEvent is PointerHoverEvent ? previousEvent.position : null;
final bool pointerHasMoved = lastHoverPosition == null || lastHoverPosition != unhandledEvent.position;
// If the hover event follows a non-hover event, or has moved since the
// last hover, then trigger the hover callback on all annotations.
// Otherwise, trigger the hover callback only on annotations that it
// newly enters.
final Iterable<MouseTrackerAnnotation> hoveringAnnotations = pointerHasMoved ? nextAnnotations.toList().reversed : enteringAnnotations;
for (final MouseTrackerAnnotation annotation in hoveringAnnotations) { for (final MouseTrackerAnnotation annotation in hoveringAnnotations) {
// Deduplicate: Trigger hover if it's a newly hovered annotation
// or the position has changed
assert(trackedAnnotations.contains(annotation));
if (!lastAnnotations.contains(annotation)
|| lastHoverPosition != unhandledEvent.position) {
if (annotation.onHover != null) { if (annotation.onHover != null) {
annotation.onHover(unhandledEvent); annotation.onHover(unhandledEvent);
} }
} }
} }
} }
}
bool _hasScheduledPostFrameCheck = false; bool _hasScheduledPostFrameCheck = false;
/// Mark all devices as dirty, and schedule a callback that is executed in the /// Mark all devices as dirty, and schedule a callback that is executed in the
...@@ -519,63 +466,4 @@ class MouseTracker extends ChangeNotifier { ...@@ -519,63 +466,4 @@ class MouseTracker extends ChangeNotifier {
/// Whether or not a mouse is connected and has produced events. /// Whether or not a mouse is connected and has produced events.
bool get mouseIsConnected => _mouseStates.isNotEmpty; bool get mouseIsConnected => _mouseStates.isNotEmpty;
/// Checks if the given [MouseTrackerAnnotation] is attached to this
/// [MouseTracker].
///
/// This function is only public to allow for proper testing of the
/// MouseTracker. Do not call in other contexts.
@visibleForTesting
bool isAnnotationAttached(MouseTrackerAnnotation annotation) {
return _trackedAnnotations.contains(annotation);
}
/// Notify [MouseTracker] that a new [MouseTrackerAnnotation] has started to
/// take effect.
///
/// This method is typically called by the [RenderObject] that owns an
/// annotation, as soon as the render object is added to the render tree.
///
/// {@template flutter.mouseTracker.attachAnnotation}
/// Render objects that call this method might want to schedule a frame as
/// well, typically by calling [RenderObject.markNeedsPaint], because this
/// method does not cause any immediate effect, since the state it changes is
/// used during a post-frame callback or when handling certain pointer events.
///
/// ### About annotation attachment
///
/// It is the responsibility of the render object that owns the annotation to
/// maintain the attachment of the annotation. Whether an annotation is
/// attached should be kept in sync with whether its owner object is mounted,
/// which is used in the following ways:
///
/// * If a pointer enters an annotation, it is asserted that the annotation
/// is attached.
/// * If a pointer stops being contained by an annotation,
/// the exit event is triggered only if the annotation is still attached.
/// This is to prevent exceptions caused calling setState of a disposed
/// widget. See [MouseTrackerAnnotation.onExit] for more details.
/// * The [MouseTracker] also uses the attachment to track the number of
/// attached annotations, and will skip mouse position checks if there is no
/// annotations attached.
/// {@endtemplate}
/// * Attaching an annotation that has been attached will assert.
void attachAnnotation(MouseTrackerAnnotation annotation) {
assert(!_duringDeviceUpdate);
assert(!_trackedAnnotations.contains(annotation));
_trackedAnnotations.add(annotation);
}
/// Notify [MouseTracker] that a mouse tracker annotation that was previously
/// attached has stopped taking effect.
///
/// This method is typically called by the [RenderObject] that owns an
/// annotation, as soon as the render object is removed from the render tree.
/// {@macro flutter.mouseTracker.attachAnnotation}
/// * Detaching an annotation that has not been attached will assert.
void detachAnnotation(MouseTrackerAnnotation annotation) {
assert(!_duringDeviceUpdate);
assert(_trackedAnnotations.contains(annotation));
_trackedAnnotations.remove(annotation);
}
} }
...@@ -10,7 +10,6 @@ import 'package:flutter/gestures.dart'; ...@@ -10,7 +10,6 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/semantics.dart'; import 'package:flutter/semantics.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'binding.dart';
import 'box.dart'; import 'box.dart';
import 'layer.dart'; import 'layer.dart';
import 'object.dart'; import 'object.dart';
...@@ -838,13 +837,11 @@ mixin _PlatformViewGestureMixin on RenderBox { ...@@ -838,13 +837,11 @@ mixin _PlatformViewGestureMixin on RenderBox {
if (_handlePointerEvent != null) if (_handlePointerEvent != null)
_handlePointerEvent(event); _handlePointerEvent(event);
}); });
RendererBinding.instance.mouseTracker.attachAnnotation(_hoverAnnotation);
} }
@override @override
void detach() { void detach() {
_gestureRecognizer.reset(); _gestureRecognizer.reset();
RendererBinding.instance.mouseTracker.detachAnnotation(_hoverAnnotation);
_hoverAnnotation = null; _hoverAnnotation = null;
super.detach(); super.detach();
} }
......
...@@ -2659,6 +2659,9 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior { ...@@ -2659,6 +2659,9 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior {
/// [RenderMouseRegion]. /// [RenderMouseRegion].
class RenderMouseRegion extends RenderProxyBox { class RenderMouseRegion extends RenderProxyBox {
/// Creates a render object that forwards pointer events to callbacks. /// Creates a render object that forwards pointer events to callbacks.
///
/// All parameters are optional. By default this method creates an opaque
/// mouse region with no callbacks.
RenderMouseRegion({ RenderMouseRegion({
PointerEnterEventListener onEnter, PointerEnterEventListener onEnter,
PointerHoverEventListener onHover, PointerHoverEventListener onHover,
...@@ -2698,17 +2701,23 @@ class RenderMouseRegion extends RenderProxyBox { ...@@ -2698,17 +2701,23 @@ class RenderMouseRegion extends RenderProxyBox {
set opaque(bool value) { set opaque(bool value) {
if (_opaque != value) { if (_opaque != value) {
_opaque = value; _opaque = value;
_updateAnnotations(); _markPropertyUpdated(mustRepaint: true);
} }
} }
/// Called when a mouse pointer enters the region (with or without buttons /// Called when a mouse pointer starts being contained by the region (with or
/// pressed). /// without buttons pressed) for any reason.
///
/// This callback is always matched by a later [onExit].
///
/// See also:
///
/// * [MouseRegion.onEnter], which uses this callback.
PointerEnterEventListener get onEnter => _onEnter; PointerEnterEventListener get onEnter => _onEnter;
set onEnter(PointerEnterEventListener value) { set onEnter(PointerEnterEventListener value) {
if (_onEnter != value) { if (_onEnter != value) {
_onEnter = value; _onEnter = value;
_updateAnnotations(); _markPropertyUpdated(mustRepaint: false);
} }
} }
PointerEnterEventListener _onEnter; PointerEnterEventListener _onEnter;
...@@ -2723,7 +2732,7 @@ class RenderMouseRegion extends RenderProxyBox { ...@@ -2723,7 +2732,7 @@ class RenderMouseRegion extends RenderProxyBox {
set onHover(PointerHoverEventListener value) { set onHover(PointerHoverEventListener value) {
if (_onHover != value) { if (_onHover != value) {
_onHover = value; _onHover = value;
_updateAnnotations(); _markPropertyUpdated(mustRepaint: false);
} }
} }
PointerHoverEventListener _onHover; PointerHoverEventListener _onHover;
...@@ -2732,13 +2741,20 @@ class RenderMouseRegion extends RenderProxyBox { ...@@ -2732,13 +2741,20 @@ class RenderMouseRegion extends RenderProxyBox {
_onHover(event); _onHover(event);
} }
/// Called when a pointer leaves the region (with or without buttons pressed) /// Called when a pointer is no longer contained by the region (with or
/// and the annotation is still attached. /// without buttons pressed) for any reason.
///
/// This callback is always matched by an earlier [onEnter].
///
/// See also:
///
/// * [MouseRegion.onExit], which uses this callback, but is not triggered in
/// certain cases and does not always match its earier [MouseRegion.onEnter].
PointerExitEventListener get onExit => _onExit; PointerExitEventListener get onExit => _onExit;
set onExit(PointerExitEventListener value) { set onExit(PointerExitEventListener value) {
if (_onExit != value) { if (_onExit != value) {
_onExit = value; _onExit = value;
_updateAnnotations(); _markPropertyUpdated(mustRepaint: false);
} }
} }
PointerExitEventListener _onExit; PointerExitEventListener _onExit;
...@@ -2757,64 +2773,52 @@ class RenderMouseRegion extends RenderProxyBox { ...@@ -2757,64 +2773,52 @@ class RenderMouseRegion extends RenderProxyBox {
@visibleForTesting @visibleForTesting
MouseTrackerAnnotation get hoverAnnotation => _hoverAnnotation; MouseTrackerAnnotation get hoverAnnotation => _hoverAnnotation;
void _updateAnnotations() { // Call this method when a property has changed and might affect the
final bool annotationWasActive = _annotationIsActive; // `_annotationIsActive` bit.
final bool annotationWillBeActive = ( //
// If `mustRepaint` is false, this method does NOT call `markNeedsPaint`
// unless the `_annotationIsActive` bit is changed. If there is a property
// that needs updating while `_annotationIsActive` stays true, make
// `mustRepaint` true.
//
// This method must not be called during `paint`.
void _markPropertyUpdated({@required bool mustRepaint}) {
assert(owner == null || !owner.debugDoingPaint);
final bool newAnnotationIsActive = (
_onEnter != null || _onEnter != null ||
_onHover != null || _onHover != null ||
_onExit != null || _onExit != null ||
opaque opaque
) && ) && RendererBinding.instance.mouseTracker.mouseIsConnected;
RendererBinding.instance.mouseTracker.mouseIsConnected; _setAnnotationIsActive(newAnnotationIsActive);
if (annotationWasActive != annotationWillBeActive) { if (mustRepaint)
markNeedsPaint();
}
void _setAnnotationIsActive(bool value) {
final bool annotationWasActive = _annotationIsActive;
_annotationIsActive = value;
if (annotationWasActive != value) {
markNeedsPaint(); markNeedsPaint();
markNeedsCompositingBitsUpdate(); markNeedsCompositingBitsUpdate();
if (annotationWillBeActive) {
RendererBinding.instance.mouseTracker.attachAnnotation(_hoverAnnotation);
} else {
RendererBinding.instance.mouseTracker.detachAnnotation(_hoverAnnotation);
} }
_annotationIsActive = annotationWillBeActive;
} }
void _handleUpdatedMouseIsConnected() {
_markPropertyUpdated(mustRepaint: false);
} }
@override @override
void attach(PipelineOwner owner) { void attach(PipelineOwner owner) {
super.attach(owner); super.attach(owner);
// Add a listener to listen for changes in mouseIsConnected. // Add a listener to listen for changes in mouseIsConnected.
RendererBinding.instance.mouseTracker.addListener(_updateAnnotations); RendererBinding.instance.mouseTracker.addListener(_handleUpdatedMouseIsConnected);
_updateAnnotations(); _markPropertyUpdated(mustRepaint: false);
}
/// Attaches the annotation for this render object, if any.
///
/// This is called by the [MouseRegion]'s [Element] to tell this
/// [RenderMouseRegion] that it has transitioned from "inactive"
/// state to "active". We call it here so that
/// [MouseTrackerAnnotation.onEnter] isn't called during the build step for
/// the widget that provided the callback, and [State.setState] can safely be
/// called within that callback.
void postActivate() {
if (_annotationIsActive)
RendererBinding.instance.mouseTracker.attachAnnotation(_hoverAnnotation);
}
/// Detaches the annotation for this render object, if any.
///
/// This is called by the [MouseRegion]'s [Element] to tell this
/// [RenderMouseRegion] that it will shortly be transitioned from "active"
/// state to "inactive". We call it here so that
/// [MouseTrackerAnnotation.onExit] isn't called during the build step for the
/// widget that provided the callback, and [State.setState] can safely be
/// called within that callback.
void preDeactivate() {
if (_annotationIsActive)
RendererBinding.instance.mouseTracker.detachAnnotation(_hoverAnnotation);
} }
@override @override
void detach() { void detach() {
RendererBinding.instance.mouseTracker.removeListener(_updateAnnotations); RendererBinding.instance.mouseTracker.removeListener(_handleUpdatedMouseIsConnected);
super.detach(); super.detach();
} }
......
...@@ -5854,7 +5854,7 @@ class _PointerListener extends SingleChildRenderObjectWidget { ...@@ -5854,7 +5854,7 @@ class _PointerListener extends SingleChildRenderObjectWidget {
/// ///
/// * [Listener], a similar widget that tracks pointer events when the pointer /// * [Listener], a similar widget that tracks pointer events when the pointer
/// have buttons pressed. /// have buttons pressed.
class MouseRegion extends SingleChildRenderObjectWidget { class MouseRegion extends StatefulWidget {
/// Creates a widget that forwards mouse events to callbacks. /// Creates a widget that forwards mouse events to callbacks.
const MouseRegion({ const MouseRegion({
Key key, Key key,
...@@ -5862,16 +5862,15 @@ class MouseRegion extends SingleChildRenderObjectWidget { ...@@ -5862,16 +5862,15 @@ class MouseRegion extends SingleChildRenderObjectWidget {
this.onExit, this.onExit,
this.onHover, this.onHover,
this.opaque = true, this.opaque = true,
Widget child, this.child,
}) : assert(opaque != null), }) : assert(opaque != null),
super(key: key, child: child); super(key: key);
/// Called when a mouse pointer, with or without buttons pressed, has /// Called when a mouse pointer has entered this widget.
/// entered this widget.
/// ///
/// This callback is triggered when the pointer has started to be contained /// This callback is triggered when the pointer, with or without buttons
/// by the region of this widget. More specifically, the callback is triggered /// pressed, has started to be contained by the region of this widget. More
/// by the following cases: /// specifically, the callback is triggered by the following cases:
/// ///
/// * This widget has appeared under a pointer. /// * This widget has appeared under a pointer.
/// * This widget has moved to under a pointer. /// * This widget has moved to under a pointer.
...@@ -5880,8 +5879,13 @@ class MouseRegion extends SingleChildRenderObjectWidget { ...@@ -5880,8 +5879,13 @@ class MouseRegion extends SingleChildRenderObjectWidget {
/// ///
/// This callback is not always matched by an [onExit]. If the [MouseRegion] /// This callback is not always matched by an [onExit]. If the [MouseRegion]
/// is unmounted while being hovered by a pointer, the [onExit] of the widget /// is unmounted while being hovered by a pointer, the [onExit] of the widget
/// callback will never called, despite the earlier call of [onEnter]. For /// callback will never called. For more details, see [onExit].
/// more details, see [onExit]. ///
/// {@template flutter.mouseRegion.triggerTime}
/// The time that this callback is triggered is always between frames: either
/// during the post-frame callbacks, or during the callback of a pointer
/// event.
/// {@endtemplate}
/// ///
/// See also: /// See also:
/// ///
...@@ -5890,47 +5894,182 @@ class MouseRegion extends SingleChildRenderObjectWidget { ...@@ -5890,47 +5894,182 @@ class MouseRegion extends SingleChildRenderObjectWidget {
/// internally implemented. /// internally implemented.
final PointerEnterEventListener onEnter; final PointerEnterEventListener onEnter;
/// Called when a mouse pointer changes position without buttons pressed, and /// Called when a mouse pointer moves within this widget without buttons
/// the new position is within the region defined by this widget.
///
/// This callback is triggered when:
///
/// * An annotation that did not contain the pointer has moved to under a
/// pointer that has no buttons pressed.
/// * A pointer has moved onto, or moved within an annotation without buttons
/// pressed. /// pressed.
/// ///
/// This callback is not triggered when /// This callback is not triggered when the [MouseRegion] has moved
/// while being hovered by the mouse pointer.
/// ///
/// * An annotation that is containing the pointer has moved, and still /// {@macro flutter.mouseRegion.triggerTime}
/// contains the pointer.
final PointerHoverEventListener onHover; final PointerHoverEventListener onHover;
/// Called when a mouse pointer, with or without buttons pressed, has exited /// Called when a mouse pointer has exited this widget when the widget is
/// this widget when the widget is still mounted. /// still mounted.
///
/// This callback is triggered when the pointer, with or without buttons
/// pressed, has stopped being contained by the region of this widget, except
/// when the exit is caused by the disappearance of this widget. More
/// specifically, this callback is triggered by the following cases:
///
/// * A pointer that is hovering this widget has moved away.
/// * A pointer that is hovering this widget has been removed.
/// * This widget, which is being hovered by a pointer, has moved away.
///
/// And is __not__ triggered by the following case:
///
/// * This widget, which is being hovered by a pointer, has disappeared.
///
/// This means that a [MouseRegion.onExit] might not be matched by a
/// [MouseRegion.onEnter].
///
/// This restriction aims to prevent a common misuse: if [setState] is called
/// during [MouseRegion.onExit] without checking whether the widget is still
/// mounted, an exception will occur. This is because the callback is
/// triggered during the post-frame phase, at which point the widget has been
/// unmounted. Since [setState] is exclusive to widgets, the restriction is
/// specific to [MouseRegion], and does not apply to its lower-level
/// counterparts, [RenderMouseRegion] and [MouseTrackerAnnotation].
///
/// There are a few ways to mitigate this restriction:
///
/// * If the hover state is completely contained within a widget that
/// unconditionally creates this [MouseRegion], then this will not be a
/// concern, since after the [MouseRegion] is unmounted the state is no
/// longer used.
/// * Otherwise, the outer widget very likely has access to the variable that
/// controls whether this [MouseRegion] is present. If so, call [onExit] at
/// the event that turns the condition from true to false.
/// * In cases where the solutions above won't work, you can always
/// override [State.dispose] and call [onExit], or create your own widget
/// using [RenderMouseRegion].
/// ///
/// This callback is triggered when the pointer has stopped to be contained /// {@tool sample --template=stateful_widget_scaffold_center}
/// by the region of this widget, except when it's caused by the removal of /// The following example shows a blue rectangular that turns yellow when
/// this widget. More specifically, the callback is triggered by /// hovered. Since the hover state is completely contained within a widget
/// the following cases: /// that unconditionally creates the `MouseRegion`, you can ignore the
/// aforementioned restriction.
/// ///
/// * This widget, which used to contain a pointer, has moved away. /// ```dart
/// * A pointer that used to be within this widget has been removed. /// bool hovered = false;
/// * A pointer that used to be within this widget has moved away. ///
/// @override
/// Widget build(BuildContext context) {
/// return Container(
/// height: 100,
/// width: 100,
/// decoration: BoxDecoration(color: hovered ? Colors.yellow : Colors.blue),
/// child: MouseRegion(
/// onEnter: (_) {
/// setState(() { hovered = true; });
/// },
/// onExit: (_) {
/// setState(() { hovered = false; });
/// },
/// ),
/// );
/// }
/// ```
/// {@end-tool}
/// ///
/// And is __not__ triggered by the following case, /// {@tool sample --template=stateful_widget_scaffold_center}
/// The following example shows a widget that hides its content one second
/// after behing hovered, and also exposes the enter and exit callbacks.
/// Because the widget conditionally creates the `MouseRegion`, and leaks the
/// hover state, it needs to take the restriction into consideration. In this
/// case, since it has access to the event that triggers the disappearance of
/// the `MouseRegion`, it simply trigger the exit callback during that event
/// as well.
///
/// ```dart preamble
/// // A region that hides its content one second after being hovered.
/// class MyTimedButton extends StatefulWidget {
/// MyTimedButton({ Key key, this.onEnterButton, this.onExitButton })
/// : super(key: key);
///
/// final VoidCallback onEnterButton;
/// final VoidCallback onExitButton;
///
/// @override
/// _MyTimedButton createState() => _MyTimedButton();
/// }
///
/// class _MyTimedButton extends State<MyTimedButton> {
/// bool regionIsHidden = false;
/// bool hovered = false;
///
/// void startCountdown() async {
/// await Future.delayed(const Duration(seconds: 1));
/// hideButton();
/// }
///
/// void hideButton() {
/// setState(() { regionIsHidden = true; });
/// // This statement is necessary.
/// if (hovered)
/// widget.onExitButton();
/// }
///
/// @override
/// Widget build(BuildContext context) {
/// return Container(
/// width: 100,
/// height: 100,
/// child: MouseRegion(
/// child: regionIsHidden ? null : MouseRegion(
/// onEnter: (_) {
/// widget.onEnterButton();
/// setState(() { hovered = true; });
/// startCountdown();
/// },
/// onExit: (_) {
/// setState(() { hovered = false; });
/// widget.onExitButton();
/// },
/// child: Container(color: Colors.red),
/// ),
/// ),
/// );
/// }
/// }
/// ```
/// ///
/// * This widget, which used to contain a pointer, has disappeared. /// ```dart
/// Key key = UniqueKey();
/// bool hovering = false;
///
/// @override
/// Widget build(BuildContext context) {
/// return Column(
/// children: <Widget>[
/// RaisedButton(
/// onPressed: () {
/// setState(() { key = UniqueKey(); });
/// },
/// child: Text('Refresh'),
/// ),
/// hovering ? Text('Hovering') : Text('Not hovering'),
/// MyTimedButton(
/// key: key,
/// onEnterButton: () {
/// setState(() { hovering = true; });
/// },
/// onExitButton: () {
/// setState(() { hovering = false; });
/// },
/// ),
/// ],
/// );
/// }
/// ```
/// {@end-tool}
/// ///
/// The last case is the only case when [onExit] does not match an earlier /// {@macro flutter.mouseRegion.triggerTime}
/// [onEnter].
/// {@macro flutter.mouseTracker.onExit}
/// ///
/// See also: /// See also:
/// ///
/// * [onEnter], which is triggered when a mouse pointer enters the region. /// * [onEnter], which is triggered when a mouse pointer enters the region.
/// * [MouseTrackerAnnotation.onExit], which is how this callback is /// * [RenderMouseRegion] and [MouseTrackerAnnotation.onExit], which are how
/// internally implemented. /// this callback is internally implemented, but without the restriction.
final PointerExitEventListener onExit; final PointerExitEventListener onExit;
/// Whether this widget should prevent other [MouseRegion]s visually behind it /// Whether this widget should prevent other [MouseRegion]s visually behind it
...@@ -5949,27 +6088,13 @@ class MouseRegion extends SingleChildRenderObjectWidget { ...@@ -5949,27 +6088,13 @@ class MouseRegion extends SingleChildRenderObjectWidget {
/// This defaults to true. /// This defaults to true.
final bool opaque; final bool opaque;
@override /// The widget below this widget in the tree.
_MouseRegionElement createElement() => _MouseRegionElement(this); ///
/// {@macro flutter.widgets.child}
@override final Widget child;
RenderMouseRegion createRenderObject(BuildContext context) {
return RenderMouseRegion(
onEnter: onEnter,
onHover: onHover,
onExit: onExit,
opaque: opaque,
);
}
@override @override
void updateRenderObject(BuildContext context, RenderMouseRegion renderObject) { _MouseRegionState createState() => _MouseRegionState();
renderObject
..onEnter = onEnter
..onHover = onHover
..onExit = onExit
..opaque = opaque;
}
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
...@@ -5986,21 +6111,46 @@ class MouseRegion extends SingleChildRenderObjectWidget { ...@@ -5986,21 +6111,46 @@ class MouseRegion extends SingleChildRenderObjectWidget {
} }
} }
class _MouseRegionElement extends SingleChildRenderObjectElement { class _MouseRegionState extends State<MouseRegion> {
_MouseRegionElement(SingleChildRenderObjectWidget widget) : super(widget); void handleExit(PointerExitEvent event) {
if (widget.onExit != null && mounted)
widget.onExit(event);
}
PointerExitEventListener getHandleExit() {
return widget.onExit == null ? null : handleExit;
}
@override
Widget build(BuildContext context) {
return _RawMouseRegion(this);
}
}
class _RawMouseRegion extends SingleChildRenderObjectWidget {
_RawMouseRegion(this.owner) : super(child: owner.widget.child);
final _MouseRegionState owner;
@override @override
void activate() { RenderMouseRegion createRenderObject(BuildContext context) {
super.activate(); final MouseRegion widget = owner.widget;
final RenderMouseRegion renderMouseRegion = renderObject as RenderMouseRegion; return RenderMouseRegion(
renderMouseRegion.postActivate(); onEnter: widget.onEnter,
onHover: widget.onHover,
onExit: owner.getHandleExit(),
opaque: widget.opaque,
);
} }
@override @override
void deactivate() { void updateRenderObject(BuildContext context, RenderMouseRegion renderObject) {
final RenderMouseRegion renderMouseRegion = renderObject as RenderMouseRegion; final MouseRegion widget = owner.widget;
renderMouseRegion.preDeactivate(); renderObject
super.deactivate(); ..onEnter = widget.onEnter
..onHover = widget.onHover
..onExit = owner.getHandleExit()
..opaque = widget.opaque;
} }
} }
......
...@@ -85,7 +85,6 @@ void main() { ...@@ -85,7 +85,6 @@ void main() {
yield annotation; yield annotation;
}, },
); );
_mouseTracker.attachAnnotation(annotation);
return annotation; return annotation;
} }
...@@ -102,7 +101,7 @@ void main() { ...@@ -102,7 +101,7 @@ void main() {
); );
expect( expect(
annotation1.toString(), annotation1.toString(),
equals('MouseTrackerAnnotation#${shortHash(annotation1)}(callbacks: enter hover exit)'), equals('MouseTrackerAnnotation#${shortHash(annotation1)}(callbacks: [enter, hover, exit])'),
); );
const MouseTrackerAnnotation annotation2 = MouseTrackerAnnotation(); const MouseTrackerAnnotation annotation2 = MouseTrackerAnnotation();
...@@ -249,73 +248,6 @@ void main() { ...@@ -249,73 +248,6 @@ void main() {
events.clear(); events.clear();
}); });
test('should not flip out when attaching and detaching during callbacks', () {
// It is a common pattern that a callback that listens to the changes of
// [MouseTracker.mouseIsConnected] triggers annotation attaching and
// detaching. This test ensures that no exceptions are thrown for this
// pattern.
bool isInHitRegion = false;
final List<PointerEvent> events = <PointerEvent>[];
final MouseTrackerAnnotation annotation = MouseTrackerAnnotation(
onEnter: (PointerEnterEvent event) => events.add(event),
onHover: (PointerHoverEvent event) => events.add(event),
onExit: (PointerExitEvent event) => events.add(event),
);
_setUpMouseAnnotationFinder((Offset position) sync* {
if (isInHitRegion) {
yield annotation;
}
});
void mockMarkNeedsPaint() {
_binding.scheduleMouseTrackerPostFrameCheck();
}
final VoidCallback firstListener = () {
if (!_mouseTracker.mouseIsConnected) {
_mouseTracker.detachAnnotation(annotation);
isInHitRegion = false;
} else {
_mouseTracker.attachAnnotation(annotation);
isInHitRegion = true;
}
mockMarkNeedsPaint();
};
_mouseTracker.addListener(firstListener);
// The pointer is added onto the annotation, triggering attaching callback.
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, const Offset(1.0, 0.0)),
]));
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
]));
expect(_mouseTracker.mouseIsConnected, isTrue);
_binding.flushPostFrameCallbacks(Duration.zero);
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
const PointerEnterEvent(position: Offset(1.0, 0.0)),
]));
expect(_mouseTracker.mouseIsConnected, isTrue);
events.clear();
// The pointer is removed while on the annotation, triggering dettaching callback.
_mouseTracker.removeListener(firstListener);
_mouseTracker.addListener(() {
if (!_mouseTracker.mouseIsConnected) {
_mouseTracker.detachAnnotation(annotation);
isInHitRegion = false;
}
});
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.remove, const Offset(1.0, 0.0)),
]));
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
const PointerExitEvent(position: Offset(1.0, 0.0)),
]));
expect(_mouseTracker.mouseIsConnected, isFalse);
events.clear();
});
test('should not handle non-hover events', () { test('should not handle non-hover events', () {
final List<PointerEvent> events = <PointerEvent>[]; final List<PointerEvent> events = <PointerEvent>[];
_setUpWithOneAnnotation(logEvents: events); _setUpWithOneAnnotation(logEvents: events);
...@@ -346,7 +278,7 @@ void main() { ...@@ -346,7 +278,7 @@ void main() {
events.clear(); events.clear();
}); });
test('should correctly handle when the annotation is attached or detached on the pointer', () { test('should correctly handle when the annotation appears or disappears on the pointer', () {
bool isInHitRegion; bool isInHitRegion;
final List<Object> events = <PointerEvent>[]; final List<Object> events = <PointerEvent>[];
final MouseTrackerAnnotation annotation = MouseTrackerAnnotation( final MouseTrackerAnnotation annotation = MouseTrackerAnnotation(
...@@ -371,13 +303,8 @@ void main() { ...@@ -371,13 +303,8 @@ void main() {
expect(_mouseTracker.mouseIsConnected, isTrue); expect(_mouseTracker.mouseIsConnected, isTrue);
events.clear(); events.clear();
// Attaching an annotation should trigger Enter event. // Adding an annotation should trigger Enter event.
isInHitRegion = true; isInHitRegion = true;
_mouseTracker.attachAnnotation(annotation);
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
]));
expect(_binding.postFrameCallbacks, hasLength(0));
_binding.scheduleMouseTrackerPostFrameCheck(); _binding.scheduleMouseTrackerPostFrameCheck();
expect(_binding.postFrameCallbacks, hasLength(1)); expect(_binding.postFrameCallbacks, hasLength(1));
...@@ -387,18 +314,14 @@ void main() { ...@@ -387,18 +314,14 @@ void main() {
])); ]));
events.clear(); events.clear();
// Detaching an annotation should not trigger events. // Removing an annotation should trigger events.
isInHitRegion = false; isInHitRegion = false;
_mouseTracker.detachAnnotation(annotation);
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
]));
expect(_binding.postFrameCallbacks, hasLength(0));
_binding.scheduleMouseTrackerPostFrameCheck(); _binding.scheduleMouseTrackerPostFrameCheck();
expect(_binding.postFrameCallbacks, hasLength(1)); expect(_binding.postFrameCallbacks, hasLength(1));
_binding.flushPostFrameCallbacks(Duration.zero); _binding.flushPostFrameCallbacks(Duration.zero);
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[ expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
const PointerExitEvent(position: Offset(0.0, 100.0)),
])); ]));
expect(_binding.postFrameCallbacks, hasLength(0)); expect(_binding.postFrameCallbacks, hasLength(0));
}); });
...@@ -417,8 +340,6 @@ void main() { ...@@ -417,8 +340,6 @@ void main() {
} }
}); });
// Start with an annotation attached.
_mouseTracker.attachAnnotation(annotation);
isInHitRegion = false; isInHitRegion = false;
// Connect a mouse. // Connect a mouse.
...@@ -468,8 +389,6 @@ void main() { ...@@ -468,8 +389,6 @@ void main() {
} }
}); });
// Start with an annotation attached.
_mouseTracker.attachAnnotation(annotation);
isInHitRegion = false; isInHitRegion = false;
// Connect a mouse in the region. Should trigger Enter. // Connect a mouse in the region. Should trigger Enter.
...@@ -508,8 +427,6 @@ void main() { ...@@ -508,8 +427,6 @@ void main() {
} }
}); });
// Start with annotation and mouse attached.
_mouseTracker.attachAnnotation(annotation);
isInHitRegion = false; isInHitRegion = false;
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[ ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, const Offset(200.0, 100.0)), _pointerData(PointerChange.add, const Offset(200.0, 100.0)),
...@@ -541,75 +458,17 @@ void main() { ...@@ -541,75 +458,17 @@ void main() {
])); ]));
}); });
test('should correctly handle when annotation is attached or detached while not containing the pointer', () {
final List<PointerEvent> events = <PointerEvent>[];
final MouseTrackerAnnotation annotation = MouseTrackerAnnotation(
onEnter: (PointerEnterEvent event) => events.add(event),
onHover: (PointerHoverEvent event) => events.add(event),
onExit: (PointerExitEvent event) => events.add(event),
);
_setUpMouseAnnotationFinder((Offset position) sync* {
// This annotation is never in the region.
});
// Connect a mouse when there is no annotation.
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, const Offset(0.0, 100.0)),
]));
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
]));
expect(_mouseTracker.mouseIsConnected, isTrue);
events.clear();
// Attaching an annotation should not trigger events.
_mouseTracker.attachAnnotation(annotation);
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
]));
expect(_binding.postFrameCallbacks, hasLength(0));
_binding.scheduleMouseTrackerPostFrameCheck();
expect(_binding.postFrameCallbacks, hasLength(1));
_binding.flushPostFrameCallbacks(Duration.zero);
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
]));
events.clear();
// Detaching an annotation should not trigger events.
_mouseTracker.detachAnnotation(annotation);
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
]));
expect(_binding.postFrameCallbacks, hasLength(0));
_binding.scheduleMouseTrackerPostFrameCheck();
expect(_binding.postFrameCallbacks, hasLength(1));
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.remove, const Offset(0.0, 100.0)),
]));
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
]));
});
test('should not schedule postframe callbacks when no mouse is connected', () { test('should not schedule postframe callbacks when no mouse is connected', () {
const MouseTrackerAnnotation annotation = MouseTrackerAnnotation();
_setUpMouseAnnotationFinder((Offset position) sync* { _setUpMouseAnnotationFinder((Offset position) sync* {
}); });
// This device only supports touching // Connect a touch device, which should not be recognized by MouseTracker
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[ ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, const Offset(0.0, 100.0), kind: PointerDeviceKind.touch), _pointerData(PointerChange.add, const Offset(0.0, 100.0), kind: PointerDeviceKind.touch),
])); ]));
expect(_mouseTracker.mouseIsConnected, isFalse); expect(_mouseTracker.mouseIsConnected, isFalse);
// Attaching an annotation just in case
_mouseTracker.attachAnnotation(annotation);
expect(_binding.postFrameCallbacks, hasLength(0)); expect(_binding.postFrameCallbacks, hasLength(0));
_binding.scheduleMouseTrackerPostFrameCheck();
expect(_binding.postFrameCallbacks, hasLength(0));
_mouseTracker.detachAnnotation(annotation);
}); });
test('should not flip out if not all mouse events are listened to', () { test('should not flip out if not all mouse events are listened to', () {
...@@ -628,60 +487,15 @@ void main() { ...@@ -628,60 +487,15 @@ void main() {
yield annotation2; yield annotation2;
}); });
final ui.PointerDataPacket packet = ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, const Offset(0.0, 101.0)),
_pointerData(PointerChange.hover, const Offset(1.0, 101.0)),
]);
isInHitRegionOne = false; isInHitRegionOne = false;
isInHitRegionTwo = true; isInHitRegionTwo = true;
_mouseTracker.attachAnnotation(annotation2);
ui.window.onPointerDataPacket(packet);
_mouseTracker.detachAnnotation(annotation2);
isInHitRegionTwo = false;
// Passes if no errors are thrown.
});
test('should not call annotationFinder when no annotations are attached', () {
final MouseTrackerAnnotation annotation = MouseTrackerAnnotation(
onEnter: (PointerEnterEvent event) {},
);
int finderCalled = 0;
_setUpMouseAnnotationFinder((Offset position) sync* {
finderCalled++;
// This annotation is never in the region.
});
// When no annotations are attached, hovering should not call finder.
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[ ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, const Offset(0.0, 101.0)), _pointerData(PointerChange.add, const Offset(0.0, 101.0)),
_pointerData(PointerChange.hover, const Offset(1.0, 101.0)),
])); ]));
expect(finderCalled, 0);
// Attaching should not call finder.
_mouseTracker.attachAnnotation(annotation);
_binding.flushPostFrameCallbacks(Duration.zero);
expect(finderCalled, 0);
// When annotations are attached, hovering should call finder.
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.hover, const Offset(0.0, 201.0)),
]));
expect(finderCalled, 1);
finderCalled = 0;
// Detaching an annotation should not call finder.
_mouseTracker.detachAnnotation(annotation);
_binding.flushPostFrameCallbacks(Duration.zero);
expect(finderCalled, 0);
// When all annotations are detached, hovering should not call finder. // Passes if no errors are thrown.
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.hover, const Offset(0.0, 201.0)),
]));
expect(finderCalled, 0);
}); });
test('should trigger callbacks between parents and children in correct order', () { test('should trigger callbacks between parents and children in correct order', () {
...@@ -713,8 +527,6 @@ void main() { ...@@ -713,8 +527,6 @@ void main() {
yield annotationA; yield annotationA;
} }
}); });
_mouseTracker.attachAnnotation(annotationA);
_mouseTracker.attachAnnotation(annotationB);
// Starts out of A. // Starts out of A.
isInB = false; isInB = false;
...@@ -768,8 +580,6 @@ void main() { ...@@ -768,8 +580,6 @@ void main() {
yield annotationB; yield annotationB;
} }
}); });
_mouseTracker.attachAnnotation(annotationA);
_mouseTracker.attachAnnotation(annotationB);
// Starts within A. // Starts within A.
isInA = true; isInA = true;
...@@ -868,13 +678,13 @@ class _EventCriticalFieldsMatcher extends Matcher { ...@@ -868,13 +678,13 @@ class _EventCriticalFieldsMatcher extends Matcher {
return mismatchDescription return mismatchDescription
.add('is ') .add('is ')
.addDescriptionOf(item.runtimeType) .addDescriptionOf(item.runtimeType)
.add(' and doesn\'t match ') .add(" and doesn't match ")
.addDescriptionOf(_expected.runtimeType); .addDescriptionOf(_expected.runtimeType);
} }
return mismatchDescription return mismatchDescription
.add('has ') .add('has ')
.addDescriptionOf(matchState['actual']) .addDescriptionOf(matchState['actual'])
.add(' at field `${matchState['field']}`, which doesn\'t match the expected ') .add(" at field `${matchState['field']}`, which doesn't match the expected ")
.addDescriptionOf(matchState['expected']); .addDescriptionOf(matchState['expected']);
} }
} }
...@@ -940,7 +750,7 @@ class _EventListCriticalFieldsMatcher extends Matcher { ...@@ -940,7 +750,7 @@ class _EventListCriticalFieldsMatcher extends Matcher {
mismatchDescription mismatchDescription
.add('has\n ') .add('has\n ')
.addDescriptionOf(matchState['actual']) .addDescriptionOf(matchState['actual'])
.add('\nat index ${matchState['index']}, which doesn\'t match\n ') .add("\nat index ${matchState['index']}, which doesn't match\n ")
.addDescriptionOf(matchState['expected']) .addDescriptionOf(matchState['expected'])
.add('\nsince it '); .add('\nsince it ');
final Description subDescription = StringDescription(); final Description subDescription = StringDescription();
......
...@@ -467,6 +467,20 @@ void main() { ...@@ -467,6 +467,20 @@ void main() {
// transform -> clip // transform -> clip
_testFittedBoxWithClipRectLayer(); _testFittedBoxWithClipRectLayer();
}); });
test('RenderMouseRegion can change properties when detached', () {
renderer.initMouseTracker(MouseTracker(
renderer.pointerRouter,
(_) => <MouseTrackerAnnotation>[],
));
final RenderMouseRegion object = RenderMouseRegion();
object
..opaque = false
..onEnter = (_) {}
..onExit = (_) {}
..onHover = (_) {};
// Passes if no error is thrown
});
} }
class _TestRectClipper extends CustomClipper<Rect> { class _TestRectClipper extends CustomClipper<Rect> {
......
...@@ -162,7 +162,6 @@ void main() { ...@@ -162,7 +162,6 @@ void main() {
), ),
), ),
); );
final RenderMouseRegion renderListener = tester.renderObject(find.byType(MouseRegion));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: const Offset(400.0, 300.0)); await gesture.addPointer(location: const Offset(400.0, 300.0));
addTearDown(gesture.removePointer); addTearDown(gesture.removePointer);
...@@ -178,7 +177,6 @@ void main() { ...@@ -178,7 +177,6 @@ void main() {
), ),
)); ));
expect(exit, isNull); expect(exit, isNull);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener.hoverAnnotation), isFalse);
}); });
testWidgets('Hover works with nested listeners', (WidgetTester tester) async { testWidgets('Hover works with nested listeners', (WidgetTester tester) async {
final UniqueKey key1 = UniqueKey(); final UniqueKey key1 = UniqueKey();
...@@ -229,9 +227,6 @@ void main() { ...@@ -229,9 +227,6 @@ void main() {
], ],
), ),
); );
final List<RenderObject> listeners = tester.renderObjectList(find.byType(MouseRegion)).toList();
final RenderMouseRegion renderListener1 = listeners[0] as RenderMouseRegion;
final RenderMouseRegion renderListener2 = listeners[1] as RenderMouseRegion;
Offset center = tester.getCenter(find.byKey(key2)); Offset center = tester.getCenter(find.byKey(key2));
await gesture.moveTo(center); await gesture.moveTo(center);
await tester.pump(); await tester.pump();
...@@ -243,8 +238,6 @@ void main() { ...@@ -243,8 +238,6 @@ void main() {
expect(enter1, isNotEmpty); expect(enter1, isNotEmpty);
expect(enter1.last.position, equals(center)); expect(enter1.last.position, equals(center));
expect(exit1, isEmpty); expect(exit1, isEmpty);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isTrue);
clearLists(); clearLists();
// Now make sure that exiting the child only triggers the child exit, not // Now make sure that exiting the child only triggers the child exit, not
...@@ -259,8 +252,6 @@ void main() { ...@@ -259,8 +252,6 @@ void main() {
expect(move1.last.position, equals(center)); expect(move1.last.position, equals(center));
expect(enter1, isEmpty); expect(enter1, isEmpty);
expect(exit1, isEmpty); expect(exit1, isEmpty);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isTrue);
clearLists(); clearLists();
}); });
testWidgets('Hover transfers between two listeners', (WidgetTester tester) async { testWidgets('Hover transfers between two listeners', (WidgetTester tester) async {
...@@ -314,9 +305,6 @@ void main() { ...@@ -314,9 +305,6 @@ void main() {
], ],
), ),
); );
final List<RenderObject> listeners = tester.renderObjectList(find.byType(MouseRegion)).toList();
final RenderMouseRegion renderListener1 = listeners[0] as RenderMouseRegion;
final RenderMouseRegion renderListener2 = listeners[1] as RenderMouseRegion;
final Offset center1 = tester.getCenter(find.byKey(key1)); final Offset center1 = tester.getCenter(find.byKey(key1));
final Offset center2 = tester.getCenter(find.byKey(key2)); final Offset center2 = tester.getCenter(find.byKey(key2));
await gesture.moveTo(center1); await gesture.moveTo(center1);
...@@ -329,8 +317,6 @@ void main() { ...@@ -329,8 +317,6 @@ void main() {
expect(move2, isEmpty); expect(move2, isEmpty);
expect(enter2, isEmpty); expect(enter2, isEmpty);
expect(exit2, isEmpty); expect(exit2, isEmpty);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isTrue);
clearLists(); clearLists();
await gesture.moveTo(center2); await gesture.moveTo(center2);
await tester.pump(); await tester.pump();
...@@ -343,8 +329,6 @@ void main() { ...@@ -343,8 +329,6 @@ void main() {
expect(enter2, isNotEmpty); expect(enter2, isNotEmpty);
expect(enter2.last.position, equals(center2)); expect(enter2.last.position, equals(center2));
expect(exit2, isEmpty); expect(exit2, isEmpty);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isTrue);
clearLists(); clearLists();
await gesture.moveTo(const Offset(400.0, 450.0)); await gesture.moveTo(const Offset(400.0, 450.0));
await tester.pump(); await tester.pump();
...@@ -355,8 +339,6 @@ void main() { ...@@ -355,8 +339,6 @@ void main() {
expect(enter2, isEmpty); expect(enter2, isEmpty);
expect(exit2, isNotEmpty); expect(exit2, isNotEmpty);
expect(exit2.last.position, equals(const Offset(400.0, 450.0))); expect(exit2.last.position, equals(const Offset(400.0, 450.0)));
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isTrue);
clearLists(); clearLists();
await tester.pumpWidget(Container()); await tester.pumpWidget(Container());
expect(move1, isEmpty); expect(move1, isEmpty);
...@@ -365,8 +347,6 @@ void main() { ...@@ -365,8 +347,6 @@ void main() {
expect(move2, isEmpty); expect(move2, isEmpty);
expect(enter2, isEmpty); expect(enter2, isEmpty);
expect(exit2, isEmpty); expect(exit2, isEmpty);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isFalse);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isFalse);
}); });
testWidgets('needsCompositing set when parent class needsCompositing is set', (WidgetTester tester) async { testWidgets('needsCompositing set when parent class needsCompositing is set', (WidgetTester tester) async {
......
...@@ -261,7 +261,6 @@ void main() { ...@@ -261,7 +261,6 @@ void main() {
onExit: (PointerExitEvent details) => exit = details, onExit: (PointerExitEvent details) => exit = details,
), ),
)); ));
final RenderMouseRegion renderListener = tester.renderObject(find.byType(MouseRegion));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: Offset.zero); await gesture.addPointer(location: Offset.zero);
addTearDown(gesture.removePointer); addTearDown(gesture.removePointer);
...@@ -279,7 +278,6 @@ void main() { ...@@ -279,7 +278,6 @@ void main() {
expect(enter, isNull); expect(enter, isNull);
expect(move, isNull); expect(move, isNull);
expect(exit, isNull); expect(exit, isNull);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener.hoverAnnotation), isFalse);
}); });
testWidgets('triggers pointer enter when widget moves in', (WidgetTester tester) async { testWidgets('triggers pointer enter when widget moves in', (WidgetTester tester) async {
...@@ -415,8 +413,6 @@ void main() { ...@@ -415,8 +413,6 @@ void main() {
], ],
), ),
); );
final RenderMouseRegion renderListener1 = tester.renderObject(find.byKey(key1));
final RenderMouseRegion renderListener2 = tester.renderObject(find.byKey(key2));
Offset center = tester.getCenter(find.byKey(key2)); Offset center = tester.getCenter(find.byKey(key2));
await gesture.moveTo(center); await gesture.moveTo(center);
await tester.pump(); await tester.pump();
...@@ -428,8 +424,6 @@ void main() { ...@@ -428,8 +424,6 @@ void main() {
expect(enter1, isNotEmpty); expect(enter1, isNotEmpty);
expect(enter1.last.position, equals(center)); expect(enter1.last.position, equals(center));
expect(exit1, isEmpty); expect(exit1, isEmpty);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isTrue);
clearLists(); clearLists();
// Now make sure that exiting the child only triggers the child exit, not // Now make sure that exiting the child only triggers the child exit, not
...@@ -444,8 +438,6 @@ void main() { ...@@ -444,8 +438,6 @@ void main() {
expect(move1.last.position, equals(center)); expect(move1.last.position, equals(center));
expect(enter1, isEmpty); expect(enter1, isEmpty);
expect(exit1, isEmpty); expect(exit1, isEmpty);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isTrue);
clearLists(); clearLists();
}); });
...@@ -500,8 +492,6 @@ void main() { ...@@ -500,8 +492,6 @@ void main() {
], ],
), ),
); );
final RenderMouseRegion renderListener1 = tester.renderObject(find.byKey(key1));
final RenderMouseRegion renderListener2 = tester.renderObject(find.byKey(key2));
final Offset center1 = tester.getCenter(find.byKey(key1)); final Offset center1 = tester.getCenter(find.byKey(key1));
final Offset center2 = tester.getCenter(find.byKey(key2)); final Offset center2 = tester.getCenter(find.byKey(key2));
await gesture.moveTo(center1); await gesture.moveTo(center1);
...@@ -514,8 +504,6 @@ void main() { ...@@ -514,8 +504,6 @@ void main() {
expect(move2, isEmpty); expect(move2, isEmpty);
expect(enter2, isEmpty); expect(enter2, isEmpty);
expect(exit2, isEmpty); expect(exit2, isEmpty);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isTrue);
clearLists(); clearLists();
await gesture.moveTo(center2); await gesture.moveTo(center2);
await tester.pump(); await tester.pump();
...@@ -528,8 +516,6 @@ void main() { ...@@ -528,8 +516,6 @@ void main() {
expect(enter2, isNotEmpty); expect(enter2, isNotEmpty);
expect(enter2.last.position, equals(center2)); expect(enter2.last.position, equals(center2));
expect(exit2, isEmpty); expect(exit2, isEmpty);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isTrue);
clearLists(); clearLists();
await gesture.moveTo(const Offset(400.0, 450.0)); await gesture.moveTo(const Offset(400.0, 450.0));
await tester.pump(); await tester.pump();
...@@ -540,8 +526,6 @@ void main() { ...@@ -540,8 +526,6 @@ void main() {
expect(enter2, isEmpty); expect(enter2, isEmpty);
expect(exit2, isNotEmpty); expect(exit2, isNotEmpty);
expect(exit2.last.position, equals(const Offset(400.0, 450.0))); expect(exit2.last.position, equals(const Offset(400.0, 450.0)));
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isTrue);
clearLists(); clearLists();
await tester.pumpWidget(Container()); await tester.pumpWidget(Container());
expect(move1, isEmpty); expect(move1, isEmpty);
...@@ -550,8 +534,6 @@ void main() { ...@@ -550,8 +534,6 @@ void main() {
expect(move2, isEmpty); expect(move2, isEmpty);
expect(enter2, isEmpty); expect(enter2, isEmpty);
expect(exit2, isEmpty); expect(exit2, isEmpty);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isFalse);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isFalse);
}); });
testWidgets('MouseRegion uses updated callbacks', (WidgetTester tester) async { testWidgets('MouseRegion uses updated callbacks', (WidgetTester tester) async {
...@@ -713,8 +695,8 @@ void main() { ...@@ -713,8 +695,8 @@ void main() {
child: const MouseRegion(opaque: false), child: const MouseRegion(opaque: false),
), ),
); );
final RenderMouseRegion listener = tester.renderObject(find.byType(MouseRegion)); final RenderMouseRegion mouseRegion = tester.renderObject(find.byType(MouseRegion));
expect(listener.needsCompositing, isFalse); expect(mouseRegion.needsCompositing, isFalse);
// No TransformLayer for `Transform.scale` is added because composting is // No TransformLayer for `Transform.scale` is added because composting is
// not required and therefore the transform is executed on the canvas // not required and therefore the transform is executed on the canvas
// directly. (One TransformLayer is always present for the root // directly. (One TransformLayer is always present for the root
...@@ -731,7 +713,7 @@ void main() { ...@@ -731,7 +713,7 @@ void main() {
), ),
), ),
); );
expect(listener.needsCompositing, isTrue); expect(mouseRegion.needsCompositing, isTrue);
// Compositing is required, therefore a dedicated TransformLayer for // Compositing is required, therefore a dedicated TransformLayer for
// `Transform.scale` is added. // `Transform.scale` is added.
expect(tester.layers.whereType<TransformLayer>(), hasLength(2)); expect(tester.layers.whereType<TransformLayer>(), hasLength(2));
...@@ -742,7 +724,7 @@ void main() { ...@@ -742,7 +724,7 @@ void main() {
child: const MouseRegion(opaque: false), child: const MouseRegion(opaque: false),
), ),
); );
expect(listener.needsCompositing, isFalse); expect(mouseRegion.needsCompositing, isFalse);
// TransformLayer for `Transform.scale` is removed again as transform is // TransformLayer for `Transform.scale` is removed again as transform is
// executed directly on the canvas. // executed directly on the canvas.
expect(tester.layers.whereType<TransformLayer>(), hasLength(1)); expect(tester.layers.whereType<TransformLayer>(), hasLength(1));
...@@ -756,7 +738,7 @@ void main() { ...@@ -756,7 +738,7 @@ void main() {
), ),
), ),
); );
expect(listener.needsCompositing, isTrue); expect(mouseRegion.needsCompositing, isTrue);
// Compositing is required, therefore a dedicated TransformLayer for // Compositing is required, therefore a dedicated TransformLayer for
// `Transform.scale` is added. // `Transform.scale` is added.
expect(tester.layers.whereType<TransformLayer>(), hasLength(2)); expect(tester.layers.whereType<TransformLayer>(), hasLength(2));
...@@ -767,20 +749,20 @@ void main() { ...@@ -767,20 +749,20 @@ void main() {
addTearDown(gesture.removePointer); addTearDown(gesture.removePointer);
await gesture.addPointer(location: Offset.zero); await gesture.addPointer(location: Offset.zero);
int numEntries = 0; int numEntrances = 0;
int numExits = 0; int numExits = 0;
await tester.pumpWidget( await tester.pumpWidget(
Center( Center(
child: HoverFeedback( child: HoverFeedback(
onEnter: () => numEntries++, onEnter: () { numEntrances += 1; },
onExit: () => numExits++, onExit: () { numExits += 1; },
)), )),
); );
await gesture.moveTo(tester.getCenter(find.byType(Text))); await gesture.moveTo(tester.getCenter(find.byType(Text)));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(numEntries, equals(1)); expect(numEntrances, equals(1));
expect(numExits, equals(0)); expect(numExits, equals(0));
expect(find.text('HOVERING'), findsOneWidget); expect(find.text('HOVERING'), findsOneWidget);
...@@ -788,18 +770,18 @@ void main() { ...@@ -788,18 +770,18 @@ void main() {
Container(), Container(),
); );
await tester.pump(); await tester.pump();
expect(numEntries, equals(1)); expect(numEntrances, equals(1));
expect(numExits, equals(0)); expect(numExits, equals(0));
await tester.pumpWidget( await tester.pumpWidget(
Center( Center(
child: HoverFeedback( child: HoverFeedback(
onEnter: () => numEntries++, onEnter: () { numEntrances += 1; },
onExit: () => numExits++, onExit: () { numExits += 1; },
)), )),
); );
await tester.pump(); await tester.pump();
expect(numEntries, equals(2)); expect(numEntrances, equals(2));
expect(numExits, equals(0)); expect(numExits, equals(0));
}); });
...@@ -809,21 +791,21 @@ void main() { ...@@ -809,21 +791,21 @@ void main() {
await gesture.addPointer(); await gesture.addPointer();
addTearDown(gesture.removePointer); addTearDown(gesture.removePointer);
int numEntries = 0; int numEntrances = 0;
int numExits = 0; int numExits = 0;
await tester.pumpWidget( await tester.pumpWidget(
Center( Center(
child: HoverFeedback( child: HoverFeedback(
key: feedbackKey, key: feedbackKey,
onEnter: () => numEntries++, onEnter: () { numEntrances += 1; },
onExit: () => numExits++, onExit: () { numExits += 1; },
)), )),
); );
await gesture.moveTo(tester.getCenter(find.byType(Text))); await gesture.moveTo(tester.getCenter(find.byType(Text)));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(numEntries, equals(1)); expect(numEntrances, equals(1));
expect(numExits, equals(0)); expect(numExits, equals(0));
expect(find.text('HOVERING'), findsOneWidget); expect(find.text('HOVERING'), findsOneWidget);
...@@ -832,18 +814,20 @@ void main() { ...@@ -832,18 +814,20 @@ void main() {
child: Container( child: Container(
child: HoverFeedback( child: HoverFeedback(
key: feedbackKey, key: feedbackKey,
onEnter: () => numEntries++, onEnter: () { numEntrances += 1; },
onExit: () => numExits++, onExit: () { numExits += 1; },
))), ),
),
),
); );
await tester.pump(); await tester.pump();
expect(numEntries, equals(1)); expect(numEntrances, equals(1));
expect(numExits, equals(0)); expect(numExits, equals(0));
await tester.pumpWidget( await tester.pumpWidget(
Container(), Container(),
); );
await tester.pump(); await tester.pump();
expect(numEntries, equals(1)); expect(numEntrances, equals(1));
expect(numExits, equals(0)); expect(numExits, equals(0));
}); });
...@@ -920,8 +904,8 @@ void main() { ...@@ -920,8 +904,8 @@ void main() {
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: MouseRegion( child: MouseRegion(
onEnter: (PointerEnterEvent e) {}, onEnter: (PointerEnterEvent e) {},
child: _PaintDelegateWidget( child: CustomPaint(
onPaint: _VoidDelegate(() => paintCount++), painter: _DelegatedPainter(onPaint: () { paintCount += 1; }),
child: const Text('123'), child: const Text('123'),
), ),
), ),
...@@ -943,8 +927,8 @@ void main() { ...@@ -943,8 +927,8 @@ void main() {
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: MouseRegion( child: MouseRegion(
onEnter: (PointerEnterEvent e) {}, onEnter: (PointerEnterEvent e) {},
child: _PaintDelegateWidget( child: CustomPaint(
onPaint: _VoidDelegate(() => paintCount++), painter: _DelegatedPainter(onPaint: () { paintCount += 1; }),
child: const Text('123'), child: const Text('123'),
), ),
), ),
...@@ -1325,7 +1309,118 @@ void main() { ...@@ -1325,7 +1309,118 @@ void main() {
expect(bottomRegionIsHovered, isFalse); expect(bottomRegionIsHovered, isFalse);
}); });
testWidgets('RenderMouseRegion\'s debugFillProperties when default', (WidgetTester tester) async { testWidgets("Changing MouseRegion's callbacks is effective and doesn't repaint", (WidgetTester tester) async {
final List<String> logs = <String>[];
const Key key = ValueKey<int>(1);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: const Offset(20, 20));
addTearDown(gesture.removePointer);
await tester.pumpWidget(_Scaffold(
topLeft: Container(
height: 10,
width: 10,
child: MouseRegion(
onEnter: (_) { logs.add('enter1'); },
onHover: (_) { logs.add('hover1'); },
onExit: (_) { logs.add('exit1'); },
child: CustomPaint(
painter: _DelegatedPainter(onPaint: () { logs.add('paint'); }, key: key),
),
),
),
));
expect(logs, <String>['paint']);
logs.clear();
await gesture.moveTo(const Offset(5, 5));
expect(logs, <String>['enter1', 'hover1']);
logs.clear();
await tester.pumpWidget(_Scaffold(
topLeft: Container(
height: 10,
width: 10,
child: MouseRegion(
onEnter: (_) { logs.add('enter2'); },
onHover: (_) { logs.add('hover2'); },
onExit: (_) { logs.add('exit2'); },
child: CustomPaint(
painter: _DelegatedPainter(onPaint: () { logs.add('paint'); }, key: key),
),
),
),
));
expect(logs, isEmpty);
await gesture.moveTo(const Offset(6, 6));
expect(logs, <String>['hover2']);
logs.clear();
// Compare: It repaints if the MouseRegion is unactivated.
await tester.pumpWidget(_Scaffold(
topLeft: Container(
height: 10,
width: 10,
child: MouseRegion(
opaque: false,
child: CustomPaint(
painter: _DelegatedPainter(onPaint: () { logs.add('paint'); }, key: key),
),
),
),
));
expect(logs, <String>['paint']);
});
testWidgets('Changing MouseRegion.opaque is effective and repaints', (WidgetTester tester) async {
final List<String> logs = <String>[];
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: const Offset(5, 5));
addTearDown(gesture.removePointer);
final PointerHoverEventListener onHover = (_) {};
final VoidCallback onPaintChild = () { logs.add('paint'); };
await tester.pumpWidget(_Scaffold(
topLeft: Container(
height: 10,
width: 10,
child: MouseRegion(
opaque: true,
// Dummy callback so that MouseRegion stays affective after opaque
// turns false.
onHover: onHover,
child: CustomPaint(painter: _DelegatedPainter(onPaint: onPaintChild)),
),
),
background: MouseRegion(onEnter: (_) { logs.add('hover-enter'); })
));
expect(logs, <String>['paint']);
logs.clear();
expect(logs, isEmpty);
logs.clear();
await tester.pumpWidget(_Scaffold(
topLeft: Container(
height: 10,
width: 10,
child: MouseRegion(
opaque: false,
onHover: onHover,
child: CustomPaint(painter: _DelegatedPainter(onPaint: onPaintChild)),
),
),
background: MouseRegion(onEnter: (_) { logs.add('hover-enter'); })
));
expect(logs, <String>['paint', 'hover-enter']);
});
testWidgets("RenderMouseRegion's debugFillProperties when default", (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
RenderMouseRegion().debugFillProperties(builder); RenderMouseRegion().debugFillProperties(builder);
...@@ -1339,7 +1434,7 @@ void main() { ...@@ -1339,7 +1434,7 @@ void main() {
]); ]);
}); });
testWidgets('RenderMouseRegion\'s debugFillProperties when full', (WidgetTester tester) async { testWidgets("RenderMouseRegion's debugFillProperties when full", (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
RenderMouseRegion( RenderMouseRegion(
onEnter: (PointerEnterEvent event) {}, onEnter: (PointerEnterEvent event) {},
...@@ -1377,64 +1472,46 @@ void main() { ...@@ -1377,64 +1472,46 @@ void main() {
await gesture.moveBy(const Offset(10.0, 10.0)); await gesture.moveBy(const Offset(10.0, 10.0));
expect(tester.binding.hasScheduledFrame, isFalse); expect(tester.binding.hasScheduledFrame, isFalse);
}); });
testWidgets("MouseTracker's attachAnnotation doesn't schedule any frames", (WidgetTester tester) async {
// This test is here because MouseTracker can't use testWidgets.
final MouseTrackerAnnotation annotation = MouseTrackerAnnotation(
onEnter: (PointerEnterEvent event) {},
onHover: (PointerHoverEvent event) {},
onExit: (PointerExitEvent event) {},
);
RendererBinding.instance.mouseTracker.attachAnnotation(annotation);
expect(tester.binding.hasScheduledFrame, isFalse);
expect(RendererBinding.instance.mouseTracker.isAnnotationAttached(annotation), isTrue);
RendererBinding.instance.mouseTracker.detachAnnotation(annotation);
});
} }
// This widget allows you to send a callback that is called during `onPaint`. // Render widget `topLeft` at the top-left corner, stacking on top of the widget
@immutable // `background`.
class _PaintDelegateWidget extends SingleChildRenderObjectWidget { class _Scaffold extends StatelessWidget {
const _PaintDelegateWidget({ const _Scaffold({this.topLeft, this.background});
Key key,
Widget child,
this.onPaint,
}) : super(key: key, child: child);
final _VoidDelegate onPaint; final Widget topLeft;
final Widget background;
@override @override
RenderObject createRenderObject(BuildContext context) { Widget build(BuildContext context) {
return _PaintCallbackObject(onPaint: onPaint?.callback); return Directionality(
} textDirection: TextDirection.ltr,
child: Stack(
@override children: <Widget>[
void updateRenderObject(BuildContext context, _PaintCallbackObject renderObject) { if (background != null) background,
renderObject..onPaint = onPaint?.callback; Align(
alignment: Alignment.topLeft,
child: topLeft,
),
],
),
);
} }
} }
class _VoidDelegate { class _DelegatedPainter extends CustomPainter {
_VoidDelegate(this.callback); _DelegatedPainter({this.key, this.onPaint});
final Key key;
void Function() callback; final VoidCallback onPaint;
}
class _PaintCallbackObject extends RenderProxyBox {
_PaintCallbackObject({
RenderBox child,
this.onPaint,
}) : super(child);
void Function() onPaint;
@override @override
void paint(PaintingContext context, Offset offset) { void paint(Canvas canvas, Size size) {
if (onPaint != null) {
onPaint(); onPaint();
} }
super.paint(context, offset);
} @override
bool shouldRepaint(CustomPainter oldDelegate) =>
!(oldDelegate is _DelegatedPainter && key == oldDelegate.key);
} }
class _HoverClientWithClosures extends StatefulWidget { class _HoverClientWithClosures extends StatefulWidget {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment