Unverified Commit 7f8cb7f8 authored by Tong Mu's avatar Tong Mu Committed by GitHub

System mouse cursors (#54171)

Adds the basic framework for the mouse cursor system. 

* Adds MouseRegion.cursor
* Adds SystemMouseCursors
* Adds mouseCursor to some widgets
parent a9ea085b
...@@ -45,6 +45,7 @@ export 'src/rendering/image.dart'; ...@@ -45,6 +45,7 @@ export 'src/rendering/image.dart';
export 'src/rendering/layer.dart'; export 'src/rendering/layer.dart';
export 'src/rendering/list_body.dart'; export 'src/rendering/list_body.dart';
export 'src/rendering/list_wheel_viewport.dart'; export 'src/rendering/list_wheel_viewport.dart';
export 'src/rendering/mouse_cursor.dart';
export 'src/rendering/mouse_tracking.dart'; export 'src/rendering/mouse_tracking.dart';
export 'src/rendering/object.dart'; export 'src/rendering/object.dart';
export 'src/rendering/paragraph.dart'; export 'src/rendering/paragraph.dart';
......
...@@ -142,7 +142,7 @@ class HitTestResult { ...@@ -142,7 +142,7 @@ class HitTestResult {
_debugVectorMoreOrLessEquals(transform.getColumn(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 ' '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 ' '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 ' 'point is directly under the pointing device. Did you forget to run the paint '
'matrix through PointerEvent.removePerspectiveTransform? ' 'matrix through PointerEvent.removePerspectiveTransform? '
'The provided matrix is:\n$transform' 'The provided matrix is:\n$transform'
); );
......
...@@ -40,6 +40,7 @@ class RawMaterialButton extends StatefulWidget { ...@@ -40,6 +40,7 @@ class RawMaterialButton extends StatefulWidget {
@required this.onPressed, @required this.onPressed,
this.onLongPress, this.onLongPress,
this.onHighlightChanged, this.onHighlightChanged,
this.mouseCursor,
this.textStyle, this.textStyle,
this.fillColor, this.fillColor,
this.focusColor, this.focusColor,
...@@ -102,6 +103,11 @@ class RawMaterialButton extends StatefulWidget { ...@@ -102,6 +103,11 @@ class RawMaterialButton extends StatefulWidget {
/// [State.setState] is not allowed). /// [State.setState] is not allowed).
final ValueChanged<bool> onHighlightChanged; final ValueChanged<bool> onHighlightChanged;
/// {@macro flutter.material.inkwell.mousecursor}
///
/// Defaults to null.
final MouseCursor mouseCursor;
/// Defines the default text style, with [Material.textStyle], for the /// Defines the default text style, with [Material.textStyle], for the
/// button's [child]. /// button's [child].
/// ///
...@@ -401,6 +407,7 @@ class _RawMaterialButtonState extends State<RawMaterialButton> { ...@@ -401,6 +407,7 @@ class _RawMaterialButtonState extends State<RawMaterialButton> {
onLongPress: widget.onLongPress, onLongPress: widget.onLongPress,
enableFeedback: widget.enableFeedback, enableFeedback: widget.enableFeedback,
customBorder: effectiveShape, customBorder: effectiveShape,
mouseCursor: widget.mouseCursor,
child: IconTheme.merge( child: IconTheme.merge(
data: IconThemeData(color: effectiveTextColor), data: IconThemeData(color: effectiveTextColor),
child: Container( child: Container(
......
...@@ -144,6 +144,7 @@ class FloatingActionButton extends StatelessWidget { ...@@ -144,6 +144,7 @@ class FloatingActionButton extends StatelessWidget {
@required this.onPressed, @required this.onPressed,
this.mini = false, this.mini = false,
this.shape, this.shape,
this.mouseCursor,
this.clipBehavior = Clip.none, this.clipBehavior = Clip.none,
this.focusNode, this.focusNode,
this.autofocus = false, this.autofocus = false,
...@@ -183,6 +184,7 @@ class FloatingActionButton extends StatelessWidget { ...@@ -183,6 +184,7 @@ class FloatingActionButton extends StatelessWidget {
this.disabledElevation, this.disabledElevation,
@required this.onPressed, @required this.onPressed,
this.shape, this.shape,
this.mouseCursor,
this.isExtended = true, this.isExtended = true,
this.materialTapTargetSize, this.materialTapTargetSize,
this.clipBehavior = Clip.none, this.clipBehavior = Clip.none,
...@@ -376,6 +378,11 @@ class FloatingActionButton extends StatelessWidget { ...@@ -376,6 +378,11 @@ class FloatingActionButton extends StatelessWidget {
/// shape as well. /// shape as well.
final ShapeBorder shape; final ShapeBorder shape;
/// {@macro flutter.material.inkwell.mousecursor}
///
/// Defaults to null.
final MouseCursor mouseCursor;
/// {@macro flutter.widgets.Clip} /// {@macro flutter.widgets.Clip}
/// ///
/// Defaults to [Clip.none], and must not be null. /// Defaults to [Clip.none], and must not be null.
...@@ -484,6 +491,7 @@ class FloatingActionButton extends StatelessWidget { ...@@ -484,6 +491,7 @@ class FloatingActionButton extends StatelessWidget {
Widget result = RawMaterialButton( Widget result = RawMaterialButton(
onPressed: onPressed, onPressed: onPressed,
mouseCursor: mouseCursor,
elevation: elevation, elevation: elevation,
focusElevation: focusElevation, focusElevation: focusElevation,
hoverElevation: hoverElevation, hoverElevation: hoverElevation,
......
...@@ -295,6 +295,7 @@ class InkResponse extends StatelessWidget { ...@@ -295,6 +295,7 @@ class InkResponse extends StatelessWidget {
this.onLongPress, this.onLongPress,
this.onHighlightChanged, this.onHighlightChanged,
this.onHover, this.onHover,
this.mouseCursor,
this.containedInkWell = false, this.containedInkWell = false,
this.highlightShape = BoxShape.circle, this.highlightShape = BoxShape.circle,
this.radius, this.radius,
...@@ -361,6 +362,17 @@ class InkResponse extends StatelessWidget { ...@@ -361,6 +362,17 @@ class InkResponse extends StatelessWidget {
/// material. /// material.
final ValueChanged<bool> onHover; final ValueChanged<bool> onHover;
/// {@template flutter.material.inkwell.mousecursor}
/// The cursor for a mouse pointer when it enters or is hovering over the
/// region.
///
/// If the [mouseCursor] is null, then the hovering pointer's cursor will be
/// decided by the widget behind it on the screen in hit-test order.
/// {@endtemplate}
///
/// Defaults to null.
final MouseCursor mouseCursor;
/// Whether this ink response should be clipped its bounds. /// Whether this ink response should be clipped its bounds.
/// ///
/// This flag also controls whether the splash migrates to the center of the /// This flag also controls whether the splash migrates to the center of the
...@@ -543,6 +555,7 @@ class InkResponse extends StatelessWidget { ...@@ -543,6 +555,7 @@ class InkResponse extends StatelessWidget {
onLongPress: onLongPress, onLongPress: onLongPress,
onHighlightChanged: onHighlightChanged, onHighlightChanged: onHighlightChanged,
onHover: onHover, onHover: onHover,
mouseCursor: mouseCursor,
containedInkWell: containedInkWell, containedInkWell: containedInkWell,
highlightShape: highlightShape, highlightShape: highlightShape,
radius: radius, radius: radius,
...@@ -590,6 +603,7 @@ class _InnerInkResponse extends StatefulWidget { ...@@ -590,6 +603,7 @@ class _InnerInkResponse extends StatefulWidget {
this.onLongPress, this.onLongPress,
this.onHighlightChanged, this.onHighlightChanged,
this.onHover, this.onHover,
this.mouseCursor,
this.containedInkWell = false, this.containedInkWell = false,
this.highlightShape = BoxShape.circle, this.highlightShape = BoxShape.circle,
this.radius, this.radius,
...@@ -624,6 +638,7 @@ class _InnerInkResponse extends StatefulWidget { ...@@ -624,6 +638,7 @@ class _InnerInkResponse extends StatefulWidget {
final GestureLongPressCallback onLongPress; final GestureLongPressCallback onLongPress;
final ValueChanged<bool> onHighlightChanged; final ValueChanged<bool> onHighlightChanged;
final ValueChanged<bool> onHover; final ValueChanged<bool> onHover;
final MouseCursor mouseCursor;
final bool containedInkWell; final bool containedInkWell;
final BoxShape highlightShape; final BoxShape highlightShape;
final double radius; final double radius;
...@@ -997,6 +1012,7 @@ class _InkResponseState extends State<_InnerInkResponse> ...@@ -997,6 +1012,7 @@ class _InkResponseState extends State<_InnerInkResponse>
onFocusChange: _handleFocusUpdate, onFocusChange: _handleFocusUpdate,
autofocus: widget.autofocus, autofocus: widget.autofocus,
child: MouseRegion( child: MouseRegion(
cursor: widget.mouseCursor,
onEnter: enabled ? _handleMouseEnter : null, onEnter: enabled ? _handleMouseEnter : null,
onExit: enabled ? _handleMouseExit : null, onExit: enabled ? _handleMouseExit : null,
child: GestureDetector( child: GestureDetector(
...@@ -1121,6 +1137,7 @@ class InkWell extends InkResponse { ...@@ -1121,6 +1137,7 @@ class InkWell extends InkResponse {
GestureTapCancelCallback onTapCancel, GestureTapCancelCallback onTapCancel,
ValueChanged<bool> onHighlightChanged, ValueChanged<bool> onHighlightChanged,
ValueChanged<bool> onHover, ValueChanged<bool> onHover,
MouseCursor mouseCursor,
Color focusColor, Color focusColor,
Color hoverColor, Color hoverColor,
Color highlightColor, Color highlightColor,
...@@ -1145,6 +1162,7 @@ class InkWell extends InkResponse { ...@@ -1145,6 +1162,7 @@ class InkWell extends InkResponse {
onTapCancel: onTapCancel, onTapCancel: onTapCancel,
onHighlightChanged: onHighlightChanged, onHighlightChanged: onHighlightChanged,
onHover: onHover, onHover: onHover,
mouseCursor: mouseCursor,
containedInkWell: true, containedInkWell: true,
highlightShape: BoxShape.rectangle, highlightShape: BoxShape.rectangle,
focusColor: focusColor, focusColor: focusColor,
......
// 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 'dart:async';
import 'dart:collection' show LinkedHashSet;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/services.dart';
import 'mouse_tracking.dart';
/// A mixin for [BaseMouseTracker] that sets the mouse pointer's cursors
/// on device update.
///
/// See also:
///
/// * [MouseTracker], which uses this mixin.
mixin MouseTrackerCursorMixin on BaseMouseTracker {
/// Returns the active mouse cursor of a device.
///
/// The return value is the last [MouseCursor] activated onto this
/// device, even if the activation failed.
///
/// Only valid when asserts are enabled. In release builds, always returns
/// null.
@visibleForTesting
MouseCursor debugDeviceActiveCursor(int device) {
MouseCursor result;
assert(() {
result = _lastSession[device]?.cursor;
return true;
}());
return result;
}
@protected
@override
void handleDeviceUpdate(MouseTrackerUpdateDetails details) {
super.handleDeviceUpdate(details);
_handleDeviceUpdateMouseCursor(details);
}
final Map<int, MouseCursorSession> _lastSession = <int, MouseCursorSession>{};
// Find the mouse cursor, which fallbacks to SystemMouseCursors.basic.
//
// The `annotations` is the current annotations that the device is hovering in
// visual order from front the back.
// The return value is never null.
MouseCursor _findFirstCursor(LinkedHashSet<MouseTrackerAnnotation> annotations) {
for (final MouseTrackerAnnotation annotation in annotations) {
if (annotation.cursor != null) {
return annotation.cursor;
}
}
return SystemMouseCursors.basic;
}
// Handles device update and changes mouse cursors.
void _handleDeviceUpdateMouseCursor(MouseTrackerUpdateDetails details) {
final int device = details.device;
if (details.triggeringEvent is PointerRemovedEvent) {
_lastSession.remove(device);
return;
}
final MouseCursorSession lastSession = _lastSession[device];
final MouseCursor nextCursor = _findFirstCursor(details.nextAnnotations);
if (lastSession?.cursor == nextCursor)
return;
final MouseCursorSession nextSession = nextCursor.createSession(device);
_lastSession[device] = nextSession;
lastSession?.dispose();
nextSession.activate();
}
}
/// Manages the duration that a pointing device should display a specific mouse
/// cursor.
///
/// While [MouseCursor] classes describe the kind of cursors, [MouseCursorSession]
/// classes represents a continuous use of the cursor on a pointing device. The
/// [MouseCursorSession] classes can be stateful. For example, a cursor that
/// needs to load resources might want to set a temporary cursor first, then
/// switch to the correct cursor after the load is completed.
///
/// A [MouseCursorSession] has the following lifecycle:
///
/// * When a pointing device should start displaying a cursor, [MouseTracker]
/// creates a session by calling [MouseCursor.createSession] on the target
/// cursor, and stores it in a table associated with the device.
/// * [MouseTracker] then immediately calls the session's [activate], where the
/// session should fetch resources and make system calls.
/// * When the pointing device should start displaying a different cursor,
/// [MouseTracker] calls [dispose] on this session. After [dispose], this session
/// will no longer be used in the future.
abstract class MouseCursorSession {
/// Create a session.
///
/// All arguments must be non-null.
MouseCursorSession(this.cursor, this.device)
: assert(cursor != null),
assert(device != null);
/// The cursor that created this session.
final MouseCursor cursor;
/// The device ID of the pointing device.
final int device;
/// Override this method to do the work of changing the cursor of the device.
///
/// Called right after this session is created.
///
/// This method has full control over the cursor until the [dispose] call, and
/// can make system calls to change the pointer cursor as many times as
/// necessary (usually through [SystemChannels.mouseCursor]). It can also
/// collect resources, and store the result in this object.
@protected
Future<void> activate();
/// Called when device stops displaying the cursor.
///
/// After this call, this session instance will no longer be used in the
/// future.
///
/// When implementing this method in subclasses, you should release resources
/// and prevent [activate] from causing side effects after disposal.
@protected
void dispose();
}
/// An interface for mouse cursor definitions.
///
/// A mouse cursor is a graphical image on the screen that echoes the movement
/// of a pointing device, such as a mouse or a stylus. A [MouseCursor] object
/// defines a kind of mouse cursor, such as an arrow, a pointing hand, or an
/// I-beam.
///
/// During the painting phase, [MouseCursor] objects are assigned to regions on
/// the screen via annotaions. Later during a device update (e.g. when a mouse
/// moves), [MouseTracker] finds the _active cursor_ of each mouse device, which
/// is the front-most region associated with the position of each mouse cursor,
/// or defaults to [SystemMouseCursors.basic] if no cursors are associated with
/// the position. [MouseTracker] changes the cursor of the pointer if the new
/// active cursor is different from the previous active cursor, whose effect is
/// defined by the session created by [createSession].
///
/// ## Cursor classes
///
/// A [SystemMouseCursor] is a cursor that is natively supported by the
/// platform that the program is running on. All supported system mouse cursors
/// are enumerated in [SystemMouseCursors].
///
/// ## Using cursors
///
/// A [MouseCursor] object is used by being assigned to a [MouseRegion] or
/// another widget that exposes the [MouseRegion] API, such as
/// [InkWell.mouseCursor].
///
/// {@tool snippet --template=stateless_widget_material}
/// This sample creates a rectangular region that is wrapped by a [MouseRegion]
/// with a system mouse cursor. The mouse pointer becomes an I-beam when
/// hovering over the region.
///
/// ```dart
/// Widget build(BuildContext context) {
/// return Center(
/// child: MouseRegion(
/// cursor: SystemMouseCursors.text,
/// child: Container(
/// width: 200,
/// height: 100,
/// decoration: BoxDecoration(
/// color: Colors.blue,
/// border: Border.all(color: Colors.yellow),
/// ),
/// ),
/// ),
/// );
/// }
/// ```
/// {@end-tool}
///
/// Assigning regions with mouse cursors on platforms that do not support mouse
/// cursors, or when there are no mice connected, will have no effect.
///
/// ## Related classes
///
/// [MouseCursorSession] represents the duration when a pointing device displays
/// a cursor, and defines the states and behaviors of the cursor. Every mouse
/// cursor class usually has a corresponding [MouseCursorSession] class.
///
/// [MouseTrackerCursorMixin] is a mixin that adds the feature of changing
/// cursors to [BaseMouseTracker], which tracks the relationship between mouse
/// devices and annotations. [MouseTrackerCursorMixin] is usually used as a part
/// of [MouseTracker].
@immutable
abstract class MouseCursor with Diagnosticable {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const MouseCursor();
/// Associate a pointing device to this cursor.
///
/// A mouse cursor class usually has a corresponding [MouseCursorSession]
/// class, and instantiates such class in this method.
///
/// This method is called each time a pointing device starts displaying this
/// cursor. A given cursor can be displayed by multiple devices at the same
/// time, in which case this method will be called separately for each device.
@protected
MouseCursorSession createSession(int device);
/// A very short description of the mouse cursor.
///
/// The [debugDescription] shoule be a few words that can differentiate
/// instances of a class to make debug information more readable. For example,
/// a [SystemMouseCursor] class with description "drag" will be printed as
/// "SystemMouseCursor(drag)".
///
/// The [debugDescription] must not be null, but can be an empty string.
String get debugDescription;
@override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
final String debugDescription = this.debugDescription;
if (minLevel.index >= DiagnosticLevel.info.index && debugDescription != null)
return '$runtimeType($debugDescription)';
return super.toString(minLevel: minLevel);
}
}
class _NoopMouseCursorSession extends MouseCursorSession {
_NoopMouseCursorSession(_NoopMouseCursor cursor, int device)
: super(cursor, device);
@override
Future<void> activate() async { /* Nothing */ }
@override
void dispose() { /* Nothing */ }
}
/// A mouse cursor that doesn't change the cursor when activated.
///
/// Although setting a region's cursor to [NoopMouseCursor] doesn't change the
/// cursor, it blocks regions behind it from changing the cursor, in contrast to
/// setting the cursor to null. More information about the usage of this class
/// can be found at [SystemMouseCursors.uncontrolled].
///
/// To use this class, use [SystemMouseCursors.uncontrolled]. Directly
/// instantiating this class is not allowed.
class _NoopMouseCursor extends MouseCursor {
// Application code shouldn't directly instantiate this class, since its only
// instance is accessible at [SystemMouseCursors.releaseControl].
const _NoopMouseCursor._();
@override
@protected
_NoopMouseCursorSession createSession(int device) => _NoopMouseCursorSession(this, device);
// The [debugDescription] is '' so that its toString() returns 'NoopMouseCursor()'.
@override
String get debugDescription => '';
}
class _SystemMouseCursorSession extends MouseCursorSession {
_SystemMouseCursorSession(SystemMouseCursor cursor, int device)
: super(cursor, device);
@override
SystemMouseCursor get cursor => super.cursor as SystemMouseCursor;
@override
Future<void> activate() {
return SystemChannels.mouseCursor.invokeMethod<void>(
'activateSystemCursor',
<String, dynamic>{
'device': device,
'kind': cursor.kind,
},
);
}
@override
void dispose() { /* Nothing */ }
}
/// A mouse cursor that is natively supported on the platform that the
/// application is running on.
///
/// System cursors can be used without external resources, and their appearances
/// match the experience of native apps. Examples of system cursors are a
/// pointing arrow, a pointing hand, a double arrow for resizing, or a text
/// I-beam, etc.
///
/// An instance of [SystemMouseCursor] refers to one cursor from each platform
/// that represents the same concept, such as being text text, being clickable,
/// or being a forbidden operation. Since the set of system cursors supported by
/// each platform varies, multiple instances can correspond to the same system
/// cursor.
///
/// [SystemMouseCursors] enumerates the complete set of system cursors supported
/// by Flutter, which are hard-coded in the engine. Therefore, manually
/// instantiating this class is not supported.
class SystemMouseCursor extends MouseCursor {
// Application code shouldn't directly instantiate system mouse cursors, since
// the supported system cursors are enumerated in [SystemMouseCursors].
const SystemMouseCursor._({
@required this.kind,
}) : assert(kind != null);
/// A string that identifies the kind of the cursor.
///
/// The interpretation of [kind] is platform-dependent.
final String kind;
@override
String get debugDescription => kind;
@override
@protected
_SystemMouseCursorSession createSession(int device) => _SystemMouseCursorSession(this, device);
@override
bool operator ==(dynamic other) {
if (other.runtimeType != runtimeType)
return false;
return other is SystemMouseCursor
&& other.kind == kind;
}
@override
int get hashCode => kind.hashCode;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<String>('kind', kind, level: DiagnosticLevel.debug));
}
}
/// A collection of system [MouseCursor]s.
///
/// System cursors are standard mouse cursors that are provided by the current
/// platform. They don't require external resources.
///
/// [SystemMouseCursors] is a superset of the system cursors of every platform
/// that Flutter supports, therefore some of these objects might map to the same
/// result, or fallback to the basic arrow. This mapping is defined by the
/// Flutter engine.
///
/// The cursor names are chosen to reflect the cursors' use cases instead of
/// their shapes, because different platforms might (although not commonly) use
/// different shapes for the same use case.
class SystemMouseCursors {
// This class only contains static members, and should not be instantiated or
// extended.
factory SystemMouseCursors._() => null;
/// A special value that doesn't change cursor by itself, but make a region
/// that blocks other regions behind it from changing the cursor.
///
/// When a pointer enters a region with a cursor of [uncontrolled], the pointer
/// retains its previous cursor and keeps so until it moves out of the region.
/// Technically, this region absorb the mouse cursor hit test without changing
/// the pointer's cursor.
///
/// This is useful in a region that displays a platform view, which let the
/// operating system handle pointer events and change cursors accordingly. To
/// achieve this, the region's cursor must not be any Flutter cursor, since
/// that might overwrite the system request upon pointer entering; the cursor
/// must not be null either, since that allows the widgets behind the region to
/// change cursors.
static const MouseCursor uncontrolled = _NoopMouseCursor._();
/// Hide the cursor.
///
/// Any cursor other than [none] or [uncontrolled] unhides the cursor.
static const SystemMouseCursor none = SystemMouseCursor._(kind: 'none');
/// The platform-dependent basic cursor.
///
/// Typically the shape of an arrow.
static const SystemMouseCursor basic = SystemMouseCursor._(kind: 'basic');
/// A cursor that indicates a user interface element that is clickable, such as a hyperlink.
///
/// Typically the shape of a pointing hand.
static const SystemMouseCursor click = SystemMouseCursor._(kind: 'click');
/// A cursor that indicates selectable text.
///
/// Typically the shape of a capital I.
static const SystemMouseCursor text = SystemMouseCursor._(kind: 'text');
/// A cursor that indicates a forbidden action.
///
/// Typically the shape of a circle with a diagnal line.
static const SystemMouseCursor forbidden = SystemMouseCursor._(kind: 'forbidden');
/// A cursor that indicates something that can be dragged.
///
/// Typically the shape of an open hand.
static const SystemMouseCursor grab = SystemMouseCursor._(kind: 'grab');
/// A cursor that indicates something that is being dragged.
///
/// Typically the shape of a closed hand.
static const SystemMouseCursor grabbing = SystemMouseCursor._(kind: 'grabbing');
}
...@@ -9,6 +9,9 @@ import 'package:flutter/foundation.dart'; ...@@ -9,6 +9,9 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'mouse_cursor.dart';
import 'object.dart';
/// Signature for listening to [PointerEnterEvent] events. /// Signature for listening to [PointerEnterEvent] events.
/// ///
/// Used by [MouseTrackerAnnotation], [MouseRegion] and [RenderMouseRegion]. /// Used by [MouseTrackerAnnotation], [MouseRegion] and [RenderMouseRegion].
...@@ -24,21 +27,41 @@ typedef PointerExitEventListener = void Function(PointerExitEvent event); ...@@ -24,21 +27,41 @@ typedef PointerExitEventListener = void Function(PointerExitEvent event);
/// Used by [MouseTrackerAnnotation], [MouseRegion] and [RenderMouseRegion]. /// Used by [MouseTrackerAnnotation], [MouseRegion] and [RenderMouseRegion].
typedef PointerHoverEventListener = void Function(PointerHoverEvent event); typedef PointerHoverEventListener = void Function(PointerHoverEvent event);
/// The annotation object used to annotate layers that are interested in mouse /// The annotation object used to annotate regions that are interested in mouse
/// movements. /// movements.
/// ///
/// This is added to a layer and managed by the [MouseRegion] widget. /// To use an annotation, push it with [AnnotatedRegionLayer] during painting.
/// The annotation's callbacks or configurations will be used depending on the
/// relationship between annotations and mouse pointers.
///
/// A [RenderObject] who uses this class must not dispose this class in its
/// `detach`, even if it recreates a new one in `attach`, because the object
/// might be detached and attached during the same frame during a reparent, and
/// replacing the `MouseTrackerAnnotation` will cause an unnecessary `onExit` and
/// `onEnter`.
///
/// This class is also the type parameter of the annotation search started by
/// [BaseMouseTracker].
///
/// See also:
///
/// * [BaseMouseTracker], which uses [MouseTrackerAnnotation].
class MouseTrackerAnnotation with Diagnosticable { class MouseTrackerAnnotation with Diagnosticable {
/// Creates an annotation that can be used to find layers interested in mouse /// Creates an immutable [MouseTrackerAnnotation].
/// movements. const MouseTrackerAnnotation({
const MouseTrackerAnnotation({this.onEnter, this.onHover, this.onExit}); this.onEnter,
this.onHover,
this.onExit,
this.cursor,
});
/// Triggered when a mouse pointer, with or without buttons pressed, has /// Triggered when a mouse pointer, with or without buttons pressed, has
/// entered the annotated region. /// entered the 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
/// by the annotationed region for any reason, which means it always matches a /// the region, either due to a pointer event, or due to the movement or
/// later [onExit]. /// disappearance of the region. This method is always matched by a later
/// [onExit].
/// ///
/// See also: /// See also:
/// ///
...@@ -46,37 +69,46 @@ class MouseTrackerAnnotation with Diagnosticable { ...@@ -46,37 +69,46 @@ class MouseTrackerAnnotation with Diagnosticable {
/// * [MouseRegion.onEnter], which uses this callback. /// * [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 mouse pointer has moved onto or within the region without
/// buttons pressed. /// buttons pressed.
/// ///
/// This callback is triggered when: /// This callback is not triggered by the movement of an annotation.
///
/// * 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.
/// ///
/// This callback is not triggered when: /// See also:
/// ///
/// * An annotation that is containing the pointer has moved, and still /// * [MouseRegion.onHover], which uses this callback.
/// contains the pointer.
final PointerHoverEventListener onHover; final PointerHoverEventListener onHover;
/// 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 region.
/// ///
/// This callback is triggered when the pointer has stopped being contained /// This callback is triggered when the pointer has stopped being contained
/// by the region for any reason, which means it always matches an earlier /// by the region, either due to a pointer event, or due to the movement or
/// disappearance of the region. This method always matches an earlier
/// [onEnter]. /// [onEnter].
/// ///
/// 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.
/// * [RenderMouseRegion.onExit], which uses this callback.
/// * [MouseRegion.onExit], which uses this callback, but is not triggered in /// * [MouseRegion.onExit], which uses this callback, but is not triggered in
/// certain cases and does not always match its earier [MouseRegion.onEnter]. /// certain cases and does not always match its earier [MouseRegion.onEnter].
final PointerExitEventListener onExit; final PointerExitEventListener onExit;
/// The mouse cursor for mouse pointers that are hovering over the annotated
/// region.
///
/// When a mouse enters the annotated region, its cursor will be changed to the
/// [cursor]. If the [cursor] is null, then the annotated region does not
/// control cursors, but defers the choice to the next annotation behind this
/// one on the screen in hit-test order, or [SystemMouseCursors.basic] if no
/// others can be found. When the mouse leaves the region, the cursor will be
/// set by the region found at the new location.
///
/// See also:
///
/// * [MouseRegion.cursor], which provide values to this field.
final MouseCursor cursor;
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties); super.debugFillProperties(properties);
...@@ -89,18 +121,17 @@ class MouseTrackerAnnotation with Diagnosticable { ...@@ -89,18 +121,17 @@ class MouseTrackerAnnotation with Diagnosticable {
}, },
ifEmpty: '<none>', ifEmpty: '<none>',
)); ));
properties.add(DiagnosticsProperty<MouseCursor>('cursor', cursor, defaultValue: null));
} }
} }
/// Signature for searching for [MouseTrackerAnnotation]s at the given offset. /// Signature for searching for [MouseTrackerAnnotation]s at the given offset.
/// ///
/// It is used by the [MouseTracker] to fetch annotations for the mouse /// It is used by the [BaseMouseTracker] to fetch annotations for the mouse
/// position. /// position.
typedef MouseDetectorAnnotationFinder = Iterable<MouseTrackerAnnotation> Function(Offset offset); typedef MouseDetectorAnnotationFinder = Iterable<MouseTrackerAnnotation> Function(Offset offset);
typedef _UpdatedDeviceHandler = void Function(_MouseState mouseState, LinkedHashSet<MouseTrackerAnnotation> previousAnnotations); // Various states of a connected mouse device used by [BaseMouseTracker].
// Various states of a connected mouse device used by [MouseTracker].
class _MouseState { class _MouseState {
_MouseState({ _MouseState({
@required PointerEvent initialEvent, @required PointerEvent initialEvent,
...@@ -114,6 +145,7 @@ class _MouseState { ...@@ -114,6 +145,7 @@ class _MouseState {
LinkedHashSet<MouseTrackerAnnotation> _annotations = LinkedHashSet<MouseTrackerAnnotation>(); LinkedHashSet<MouseTrackerAnnotation> _annotations = LinkedHashSet<MouseTrackerAnnotation>();
LinkedHashSet<MouseTrackerAnnotation> replaceAnnotations(LinkedHashSet<MouseTrackerAnnotation> value) { LinkedHashSet<MouseTrackerAnnotation> replaceAnnotations(LinkedHashSet<MouseTrackerAnnotation> value) {
assert(value != null);
final LinkedHashSet<MouseTrackerAnnotation> previous = _annotations; final LinkedHashSet<MouseTrackerAnnotation> previous = _annotations;
_annotations = value; _annotations = value;
return previous; return previous;
...@@ -122,9 +154,13 @@ class _MouseState { ...@@ -122,9 +154,13 @@ class _MouseState {
// The most recently processed mouse event observed from this device. // The most recently processed mouse event observed from this device.
PointerEvent get latestEvent => _latestEvent; PointerEvent get latestEvent => _latestEvent;
PointerEvent _latestEvent; PointerEvent _latestEvent;
set latestEvent(PointerEvent value) {
PointerEvent replaceLatestEvent(PointerEvent value) {
assert(value != null); assert(value != null);
assert(value.device == _latestEvent.device);
final PointerEvent previous = _latestEvent;
_latestEvent = value; _latestEvent = value;
return previous;
} }
int get device => latestEvent.device; int get device => latestEvent.device;
...@@ -140,19 +176,104 @@ class _MouseState { ...@@ -140,19 +176,104 @@ class _MouseState {
} }
} }
/// Maintains the relationship between mouse devices and /// Used by [BaseMouseTracker] to provide the details of an update of a mouse
/// [MouseTrackerAnnotation]s, and notifies interested callbacks of the changes /// device.
/// thereof. ///
/// This class contains the information needed to handle the update that might
/// change the state of a mouse device, or the [MouseTrackerAnnotation]s that
/// the mouse device is hovering.
@immutable
class MouseTrackerUpdateDetails with Diagnosticable {
/// When device update is triggered by a new frame.
///
/// All parameters are required.
const MouseTrackerUpdateDetails.byNewFrame({
@required this.lastAnnotations,
@required this.nextAnnotations,
@required this.previousEvent,
}) : assert(previousEvent != null),
assert(lastAnnotations != null),
assert(nextAnnotations != null),
triggeringEvent = null;
/// When device update is triggered by a pointer event.
///
/// The [lastAnnotations], [nextAnnotations], and [triggeringEvent] are
/// required.
const MouseTrackerUpdateDetails.byPointerEvent({
@required this.lastAnnotations,
@required this.nextAnnotations,
this.previousEvent,
@required this.triggeringEvent,
}) : assert(triggeringEvent != null),
assert(lastAnnotations != null),
assert(nextAnnotations != null);
/// The annotations that the device is hovering before the update.
///
/// It is never null.
final LinkedHashSet<MouseTrackerAnnotation> lastAnnotations;
/// The annotations that the device is hovering after the update.
///
/// It is never null.
final LinkedHashSet<MouseTrackerAnnotation> nextAnnotations;
/// The last event that the device observed before the update.
///
/// If the update is triggered by a frame, the [previousEvent] is never null,
/// since the pointer must have been added before.
///
/// If the update is triggered by a pointer event, the [previousEvent] is not
/// null except for cases where the event is the first event observed by the
/// pointer (which is not necessarily a [PointerAddedEvent]).
final PointerEvent previousEvent;
/// The event that triggered this update.
///
/// It is non-null if and only if the update is triggered by a pointer event.
final PointerEvent triggeringEvent;
/// The pointing device of this update.
int get device {
final int result = (previousEvent ?? triggeringEvent).device;
assert(result != null);
return result;
}
/// The last event that the device observed after the update.
///
/// The [latestEvent] is never null.
PointerEvent get latestEvent {
final PointerEvent result = triggeringEvent ?? previousEvent;
assert(result != null);
return result;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(IntProperty('device', device));
properties.add(DiagnosticsProperty<PointerEvent>('previousEvent', previousEvent));
properties.add(DiagnosticsProperty<PointerEvent>('triggeringEvent', triggeringEvent));
properties.add(DiagnosticsProperty<Set<MouseTrackerAnnotation>>('lastAnnotations', lastAnnotations));
properties.add(DiagnosticsProperty<Set<MouseTrackerAnnotation>>('nextAnnotations', nextAnnotations));
}
}
/// A base class that tracks the relationship between mouse devices and
/// [MouseTrackerAnnotation]s.
///
/// A _device update_ is defined as an event that changes the relationship
/// between mouse devices and [MouseTrackerAnnotation]s. Subclasses should
/// override [handleDeviceUpdate] to process the updates.
/// ///
/// This class is a [ChangeNotifier] that notifies its listeners if the value of /// This class is a [ChangeNotifier] that notifies its listeners if the value of
/// [mouseIsConnected] changes. /// [mouseIsConnected] changes.
/// ///
/// An instance of [MouseTracker] is owned by the global singleton of /// ### States and device updates
/// [RendererBinding].
///
/// ### Details
/// ///
/// The state of [MouseTracker] consists of two parts: /// The state of [BaseMouseTracker] consists of two parts:
/// ///
/// * The mouse devices that are connected. /// * The mouse devices that are connected.
/// * In which annotations each device is contained. /// * In which annotations each device is contained.
...@@ -162,22 +283,30 @@ class _MouseState { ...@@ -162,22 +283,30 @@ class _MouseState {
/// ///
/// * An eligible [PointerEvent] has been observed, e.g. a device is added, /// * An eligible [PointerEvent] has been observed, e.g. a device is added,
/// removed, or moved. In this case, the state related to this device will /// removed, or moved. In this case, the state related to this device will
/// be immediately updated. /// be immediately updated, and triggers [handleDeviceUpdate] on this device.
/// * A frame has been painted. In this case, a callback will be scheduled for /// * A frame has been painted. In this case, a callback will be scheduled for
/// the upcoming post-frame phase to update all devices. /// the upcoming post-frame phase to update all devices, and triggers
class MouseTracker extends ChangeNotifier { /// [handleDeviceUpdate] on each device separately.
/// Creates a mouse tracker to keep track of mouse locations. ///
/// See also:
///
/// * [MouseTracker], which is a subclass of [BaseMouseTracker] with definition
/// of how to process mouse event callbacks and mouse cursors.
/// * [MouseCursorMixin], which is a mixin for [BaseMouseTracker] that defines
/// how to process mouse cursors.
class BaseMouseTracker extends ChangeNotifier {
/// Creates a [BaseMouseTracker] to keep track of mouse locations.
/// ///
/// The first parameter is a [PointerRouter], which [MouseTracker] will /// The first parameter is a [PointerRouter], which [BaseMouseTracker] will
/// subscribe to and receive events from. Usually it is the global singleton /// subscribe to and receive events from. Usually it is the global singleton
/// instance [GestureBinding.pointerRouter]. /// instance [GestureBinding.pointerRouter].
/// ///
/// The second parameter is a function with which the [MouseTracker] can /// The second parameter is a function with which the [BaseMouseTracker] can
/// search for [MouseTrackerAnnotation]s at a given position. /// search for [MouseTrackerAnnotation]s at a given position.
/// Usually it is [Layer.findAllAnnotations] of the root layer. /// Usually it is [Layer.findAllAnnotations] of the root layer.
/// ///
/// All of the parameters must not be null. /// All of the parameters must be non-null.
MouseTracker(this._router, this.annotationFinder) BaseMouseTracker(this._router, this.annotationFinder)
: assert(_router != null), : assert(_router != null),
assert(annotationFinder != null) { assert(annotationFinder != null) {
_router.addGlobalRoute(_handleEvent); _router.addGlobalRoute(_handleEvent);
...@@ -192,8 +321,8 @@ class MouseTracker extends ChangeNotifier { ...@@ -192,8 +321,8 @@ class MouseTracker extends ChangeNotifier {
/// Find annotations at a given offset in global logical coordinate space /// Find annotations at a given offset in global logical coordinate space
/// in visual order from front to back. /// in visual order from front to back.
/// ///
/// [MouseTracker] uses this callback to know which annotations are affected /// [BaseMouseTracker] uses this callback to know which annotations are
/// by each device. /// affected by each device.
/// ///
/// The annotations should be returned in visual order from front to /// The annotations should be returned in visual order from front to
/// back, so that the callbacks are called in an correct order. /// back, so that the callbacks are called in an correct order.
...@@ -203,28 +332,126 @@ class MouseTracker extends ChangeNotifier { ...@@ -203,28 +332,126 @@ class MouseTracker extends ChangeNotifier {
// mouse events from. // mouse events from.
final PointerRouter _router; final PointerRouter _router;
bool _hasScheduledPostFrameCheck = false;
/// Mark all devices as dirty, and schedule a callback that is executed in the
/// upcoming post-frame phase to check their updates.
///
/// Checking a device means to collect the annotations that the pointer
/// hovers, and triggers necessary callbacks accordingly.
///
/// Although the actual callback belongs to the scheduler's post-frame phase,
/// this method must be called in persistent callback phase to ensure that
/// the callback is scheduled after every frame, since every frame can change
/// the position of annotations. Typically the method is called by
/// [RendererBinding]'s drawing method.
void schedulePostFrameCheck() {
assert(SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks);
assert(!_debugDuringDeviceUpdate);
if (!mouseIsConnected)
return;
if (!_hasScheduledPostFrameCheck) {
_hasScheduledPostFrameCheck = true;
SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
assert(_hasScheduledPostFrameCheck);
_hasScheduledPostFrameCheck = false;
_updateAllDevices();
});
}
}
/// Whether or not at least one mouse is connected and has produced events.
bool get mouseIsConnected => _mouseStates.isNotEmpty;
// 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.
final Map<int, _MouseState> _mouseStates = <int, _MouseState>{}; final Map<int, _MouseState> _mouseStates = <int, _MouseState>{};
// Used to wrap any procedure that might change `mouseIsConnected`.
//
// This method records `mouseIsConnected`, runs `task`, and calls
// [notifyListeners] at the end if the `mouseIsConnected` has changed.
void _monitorMouseConnection(VoidCallback task) {
final bool mouseWasConnected = mouseIsConnected;
task();
if (mouseWasConnected != mouseIsConnected)
notifyListeners();
}
bool _debugDuringDeviceUpdate = false;
// Used to wrap any procedure that might call [handleDeviceUpdate].
//
// In debug mode, this method uses `_debugDuringDeviceUpdate` to prevent
// `_deviceUpdatePhase` being recursively called.
void _deviceUpdatePhase(VoidCallback task) {
assert(!_debugDuringDeviceUpdate);
assert(() {
_debugDuringDeviceUpdate = true;
return true;
}());
task();
assert(() {
_debugDuringDeviceUpdate = false;
return true;
}());
}
// Whether an observed event might update a device. // Whether an observed event might update a device.
static bool _shouldMarkStateDirty(_MouseState state, PointerEvent value) { static bool _shouldMarkStateDirty(_MouseState state, PointerEvent event) {
if (state == null) if (state == null)
return true; return true;
assert(value != null); assert(event != null);
final PointerEvent lastEvent = state.latestEvent; final PointerEvent lastEvent = state.latestEvent;
assert(value.device == lastEvent.device); assert(event.device == lastEvent.device);
// An Added can only follow a Removed, and a Removed can only be followed // An Added can only follow a Removed, and a Removed can only be followed
// by an Added. // by an Added.
assert((value is PointerAddedEvent) == (lastEvent is PointerRemovedEvent)); assert((event is PointerAddedEvent) == (lastEvent is PointerRemovedEvent));
// Ignore events that are unrelated to mouse tracking. // Ignore events that are unrelated to mouse tracking.
if (value is PointerSignalEvent) if (event is PointerSignalEvent)
return false; return false;
return lastEvent is PointerAddedEvent return lastEvent is PointerAddedEvent
|| value is PointerRemovedEvent || event is PointerRemovedEvent
|| lastEvent.position != value.position; || lastEvent.position != event.position;
}
// Find the annotations that is hovered by the device of the `state`.
//
// If the device is not connected, an empty set is returned without calling
// `annotationFinder`.
LinkedHashSet<MouseTrackerAnnotation> _findAnnotations(_MouseState state) {
final Offset globalPosition = state.latestEvent.position;
final int device = state.device;
return (_mouseStates.containsKey(device))
? LinkedHashSet<MouseTrackerAnnotation>.from(annotationFinder(globalPosition))
: <MouseTrackerAnnotation>{} as LinkedHashSet<MouseTrackerAnnotation>;
}
/// A callback that is called on the update of a device.
///
/// This method should be called only by [BaseMouseTracker].
///
/// Override this method to receive updates when the relationship between a
/// device and annotations have changed. Subclasses should override this method
/// to first call to their inherited [handleDeviceUpdate] method, and then
/// process the update as desired,
///
/// The update can be caused by two kinds of triggers:
///
/// * Triggered by the addition, movement, or removal of a pointer. Such
/// calls occur during the handler of the event, indicated by
/// `details.triggeringEvent` being non-null.
/// * Triggered by the appearance, movement, or disappearance of an annotation.
/// Such calls occur after each new frame, during the post-frame callbacks,
/// indicated by `details.triggeringEvent` being null.
///
/// This method is not triggered if the [MouseTrackerAnnotation] is mutated.
///
/// Calling of this method must be wrapped in `_deviceUpdatePhase`.
@protected
@mustCallSuper
void handleDeviceUpdate(MouseTrackerUpdateDetails details) {
assert(_debugDuringDeviceUpdate);
} }
// Handler for events coming from the PointerRouter. // Handler for events coming from the PointerRouter.
...@@ -240,35 +467,32 @@ class MouseTracker extends ChangeNotifier { ...@@ -240,35 +467,32 @@ class MouseTracker extends ChangeNotifier {
if (!_shouldMarkStateDirty(existingState, event)) if (!_shouldMarkStateDirty(existingState, event))
return; return;
final PointerEvent previousEvent = existingState?.latestEvent; _monitorMouseConnection(() {
_updateDevices( _deviceUpdatePhase(() {
targetEvent: event, // Update mouseState to the latest devices that have not been removed,
handleUpdatedDevice: (_MouseState mouseState, LinkedHashSet<MouseTrackerAnnotation> previousAnnotations) { // so that [mouseIsConnected], which is decided by `_mouseStates`, is
assert(mouseState.device == event.device); // correct during the callbacks.
_dispatchDeviceCallbacks( if (existingState == null) {
lastAnnotations: previousAnnotations, _mouseStates[device] = _MouseState(initialEvent: event);
nextAnnotations: mouseState.annotations, } else {
previousEvent: previousEvent, assert(event is! PointerAddedEvent);
unhandledEvent: event, if (event is PointerRemovedEvent)
); _mouseStates.remove(event.device);
},
);
} }
final _MouseState targetState = _mouseStates[device] ?? existingState;
// Find the annotations that is hovered by the device of the `state`. final PointerEvent lastEvent = targetState.replaceLatestEvent(event);
// final LinkedHashSet<MouseTrackerAnnotation> nextAnnotations = _findAnnotations(targetState);
// If the device is not connected, an empty set is returned without calling final LinkedHashSet<MouseTrackerAnnotation> lastAnnotations = targetState.replaceAnnotations(nextAnnotations);
// `annotationFinder`.
LinkedHashSet<MouseTrackerAnnotation> _findAnnotations(_MouseState state) {
final Offset globalPosition = state.latestEvent.position;
final int device = state.device;
return (_mouseStates.containsKey(device))
? LinkedHashSet<MouseTrackerAnnotation>.from(annotationFinder(globalPosition))
: <MouseTrackerAnnotation>{} as LinkedHashSet<MouseTrackerAnnotation>;
}
static bool get _duringBuildPhase { handleDeviceUpdate(MouseTrackerUpdateDetails.byPointerEvent(
return SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks; lastAnnotations: lastAnnotations,
nextAnnotations: nextAnnotations,
previousEvent: lastEvent,
triggeringEvent: event,
));
});
});
} }
// Update all devices, despite observing no new events. // Update all devices, despite observing no new events.
...@@ -276,119 +500,37 @@ class MouseTracker extends ChangeNotifier { ...@@ -276,119 +500,37 @@ class MouseTracker extends ChangeNotifier {
// This is called after a new frame, since annotations can be moved after // This is called after a new frame, since annotations can be moved after
// every frame. // every frame.
void _updateAllDevices() { void _updateAllDevices() {
_updateDevices( _deviceUpdatePhase(() {
handleUpdatedDevice: (_MouseState mouseState, LinkedHashSet<MouseTrackerAnnotation> previousAnnotations) { for (final _MouseState dirtyState in _mouseStates.values) {
_dispatchDeviceCallbacks( final PointerEvent lastEvent = dirtyState.latestEvent;
lastAnnotations: previousAnnotations,
nextAnnotations: mouseState.annotations,
previousEvent: mouseState.latestEvent,
unhandledEvent: null,
);
}
);
}
bool _duringDeviceUpdate = false;
// Update device states with the change of a new event or a new frame, and
// trigger `handleUpdateDevice` for each dirty device.
//
// This method is called either when a new event is observed (`targetEvent`
// being non-null), or when no new event is observed but all devices are
// marked dirty due to a new frame. It means that it will not happen that all
// devices are marked dirty when a new event is unprocessed.
//
// This method is the moment where `_mouseState` is updated. Before
// this method, `_mouseState` is in sync with the state before the event or
// before the frame. During `handleUpdateDevice` and after this method,
// `_mouseState` is in sync with the state after the event or after the frame.
//
// The dirty devices are decided as follows: if `targetEvent` is not null, the
// dirty devices are the device that observed the event; otherwise all devices
// are dirty.
//
// This method first keeps `_mouseStates` up to date. More specifically,
//
// * If an event is observed, update `_mouseStates` by inserting or removing
// the state that corresponds to the event if needed, then update the
// `latestEvent` property of this mouse state.
// * For each mouse state that will correspond to a dirty device, update the
// `annotations` property with the annotations the device is contained.
//
// Then, for each dirty device, `handleUpdatedDevice` is called with the
// updated state and the annotations before the update.
//
// Last, the method checks if `mouseIsConnected` has been changed, and notify
// listeners if needed.
void _updateDevices({
PointerEvent targetEvent,
@required _UpdatedDeviceHandler handleUpdatedDevice,
}) {
assert(handleUpdatedDevice != null);
assert(!_duringBuildPhase);
assert(!_duringDeviceUpdate);
final bool mouseWasConnected = mouseIsConnected;
// If new event is not null, only the device that observed this event is
// dirty. The target device's state is inserted into or removed from
// `_mouseStates` if needed, stored as `targetState`, and its
// `mostRecentDevice` is updated.
_MouseState targetState;
if (targetEvent != null) {
targetState = _mouseStates[targetEvent.device];
if (targetState == null) {
targetState = _MouseState(initialEvent: targetEvent);
_mouseStates[targetState.device] = targetState;
} else {
assert(targetEvent is! PointerAddedEvent);
targetState.latestEvent = targetEvent;
// Update mouseState to the latest devices that have not been removed,
// so that [mouseIsConnected], which is decided by `_mouseStates`, is
// correct during the callbacks.
if (targetEvent is PointerRemovedEvent)
_mouseStates.remove(targetEvent.device);
}
}
assert((targetState == null) == (targetEvent == null));
assert(() {
_duringDeviceUpdate = true;
return true;
}());
// We can safely use `_mouseStates` here without worrying about the removed
// state, because `targetEvent` should be null when `_mouseStates` is used.
final Iterable<_MouseState> dirtyStates = targetEvent == null ? _mouseStates.values : <_MouseState>[targetState];
for (final _MouseState dirtyState in dirtyStates) {
final LinkedHashSet<MouseTrackerAnnotation> nextAnnotations = _findAnnotations(dirtyState); final LinkedHashSet<MouseTrackerAnnotation> nextAnnotations = _findAnnotations(dirtyState);
final LinkedHashSet<MouseTrackerAnnotation> lastAnnotations = dirtyState.replaceAnnotations(nextAnnotations); final LinkedHashSet<MouseTrackerAnnotation> lastAnnotations = dirtyState.replaceAnnotations(nextAnnotations);
handleUpdatedDevice(dirtyState, lastAnnotations);
}
assert(() {
_duringDeviceUpdate = false;
return true;
}());
if (mouseWasConnected != mouseIsConnected) handleDeviceUpdate(MouseTrackerUpdateDetails.byNewFrame(
notifyListeners(); lastAnnotations: lastAnnotations,
nextAnnotations: nextAnnotations,
previousEvent: lastEvent,
));
} }
});
}
}
// A mixin for [BaseMouseTracker] that dispatches mouse events on device update.
//
// See also:
//
// * [MouseTracker], which uses this mixin.
mixin _MouseTrackerEventMixin on BaseMouseTracker {
// Handles device update and dispatches mouse event callbacks.
static void _handleDeviceUpdateMouseEvents(MouseTrackerUpdateDetails details) {
final PointerEvent previousEvent = details.previousEvent;
final PointerEvent triggeringEvent = details.triggeringEvent;
final PointerEvent latestEvent = details.latestEvent;
final LinkedHashSet<MouseTrackerAnnotation> lastAnnotations = details.lastAnnotations;
final LinkedHashSet<MouseTrackerAnnotation> nextAnnotations = details.nextAnnotations;
// Dispatch callbacks related to a device after all necessary information
// has been collected.
//
// The `previousEvent` is the latest event before `unhandledEvent`. It might be
// null, which means the update is triggered by a new event.
// The `unhandledEvent` can be null, which means the update is not triggered
// by an event.
// However, one of `previousEvent` or `unhandledEvent` must not be null.
static void _dispatchDeviceCallbacks({
@required LinkedHashSet<MouseTrackerAnnotation> lastAnnotations,
@required LinkedHashSet<MouseTrackerAnnotation> nextAnnotations,
@required PointerEvent previousEvent,
@required PointerEvent unhandledEvent,
}) {
assert(lastAnnotations != null);
assert(nextAnnotations != null);
final PointerEvent latestEvent = unhandledEvent ?? previousEvent;
assert(latestEvent != null);
// Order is important for mouse event callbacks. The `findAnnotations` // Order is important for mouse event callbacks. The `findAnnotations`
// returns annotations in the visual order from front to back. We call // returns annotations in the visual order from front to back. We call
// it the "visual order", and the opposite one "reverse visual order". // it the "visual order", and the opposite one "reverse visual order".
...@@ -397,31 +539,27 @@ class MouseTracker extends ChangeNotifier { ...@@ -397,31 +539,27 @@ class MouseTracker extends ChangeNotifier {
// Send exit events to annotations that are in last but not in next, in // Send exit events to annotations that are in last but not in next, in
// visual order. // visual order.
final Iterable<MouseTrackerAnnotation> exitingAnnotations = lastAnnotations.where( final Iterable<MouseTrackerAnnotation> exitingAnnotations = lastAnnotations.difference(nextAnnotations);
(MouseTrackerAnnotation value) => !nextAnnotations.contains(value),
);
for (final MouseTrackerAnnotation annotation in exitingAnnotations) { for (final MouseTrackerAnnotation annotation in exitingAnnotations) {
if (annotation.onExit != null) { if (annotation.onExit != null)
annotation.onExit(PointerExitEvent.fromMouseEvent(latestEvent)); annotation.onExit(PointerExitEvent.fromMouseEvent(latestEvent));
} }
}
// Send enter events to annotations that are not in last but in next, in // Send enter events to annotations that are not in last but in next, in
// reverse visual order. // 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) {
if (annotation.onEnter != null) { if (annotation.onEnter != null)
annotation.onEnter(PointerEnterEvent.fromMouseEvent(latestEvent)); annotation.onEnter(PointerEnterEvent.fromMouseEvent(latestEvent));
} }
}
// Send hover events to annotations that are in next, in reverse visual // Send hover events to annotations that are in next, in reverse visual
// order. The reverse visual order is chosen only because of the simplicity // order. The reverse visual order is chosen only because of the simplicity
// by keeping the hover events aligned with enter events. // by keeping the hover events aligned with enter events.
if (unhandledEvent is PointerHoverEvent) { if (triggeringEvent is PointerHoverEvent) {
final Offset lastHoverPosition = previousEvent is PointerHoverEvent ? previousEvent.position : null; final Offset hoverPositionBeforeUpdate = previousEvent is PointerHoverEvent ? previousEvent.position : null;
final bool pointerHasMoved = lastHoverPosition == null || lastHoverPosition != unhandledEvent.position; final bool pointerHasMoved = hoverPositionBeforeUpdate == null || hoverPositionBeforeUpdate != triggeringEvent.position;
// If the hover event follows a non-hover event, or has moved since the // If the hover event follows a non-hover event, or has moved since the
// last hover, then trigger the hover callback on all annotations. // last hover, then trigger the hover callback on all annotations.
// Otherwise, trigger the hover callback only on annotations that it // Otherwise, trigger the hover callback only on annotations that it
...@@ -429,39 +567,55 @@ class MouseTracker extends ChangeNotifier { ...@@ -429,39 +567,55 @@ class MouseTracker extends ChangeNotifier {
final Iterable<MouseTrackerAnnotation> hoveringAnnotations = pointerHasMoved ? nextAnnotations.toList().reversed : enteringAnnotations; final Iterable<MouseTrackerAnnotation> hoveringAnnotations = pointerHasMoved ? nextAnnotations.toList().reversed : enteringAnnotations;
for (final MouseTrackerAnnotation annotation in hoveringAnnotations) { for (final MouseTrackerAnnotation annotation in hoveringAnnotations) {
if (annotation.onHover != null) { if (annotation.onHover != null) {
annotation.onHover(unhandledEvent); annotation.onHover(triggeringEvent);
} }
} }
} }
} }
bool _hasScheduledPostFrameCheck = false; @protected
/// Mark all devices as dirty, and schedule a callback that is executed in the @override
/// upcoming post-frame phase to check their updates. void handleDeviceUpdate(MouseTrackerUpdateDetails details) {
/// super.handleDeviceUpdate(details);
/// Checking a device means to collect the annotations that the pointer _handleDeviceUpdateMouseEvents(details);
/// hovers, and triggers necessary callbacks accordingly.
///
/// Although the actual callback belongs to the scheduler's post-frame phase,
/// this method must be called in persistent callback phase to ensure that
/// the callback is scheduled after every frame, since every frame can change
/// the position of annotations. Typically the method is called by
/// [RendererBinding]'s drawing method.
void schedulePostFrameCheck() {
assert(_duringBuildPhase);
assert(!_duringDeviceUpdate);
if (!mouseIsConnected)
return;
if (!_hasScheduledPostFrameCheck) {
_hasScheduledPostFrameCheck = true;
SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
assert(_hasScheduledPostFrameCheck);
_hasScheduledPostFrameCheck = false;
_updateAllDevices();
});
}
} }
}
/// Whether or not a mouse is connected and has produced events. /// Trackes the relationship between mouse devices and annotations, and
bool get mouseIsConnected => _mouseStates.isNotEmpty; /// triggers mouse events and cursor changes accordingly.
///
/// The [MouseTracker] trackes the relationship between mouse devices and
/// [MouseTrackerAnnotation]s, and when such relationship changes, triggers
/// the following changes if applicable:
///
/// * Dispatches mouse-related pointer events (pointer enter, hover, and exit).
/// * Notifies changes of [mouseIsConnected].
/// * Changes mouse cursors.
///
/// An instance of [MouseTracker] is owned by the global singleton of
/// [RendererBinding].
///
/// This class is a [ChangeNotifier] that notifies its listeners if the value of
/// [mouseIsConnected] changes.
///
/// See also:
///
/// * [BaseMouseTracker], which introduces more details about the timing of
/// device updates.
class MouseTracker extends BaseMouseTracker with MouseTrackerCursorMixin, _MouseTrackerEventMixin {
/// Creates a [MouseTracker] to keep track of mouse locations.
///
/// The first parameter is a [PointerRouter], which [MouseTracker] will
/// subscribe to and receive events from. Usually it is the global singleton
/// instance [GestureBinding.pointerRouter].
///
/// The second parameter is a function with which the [MouseTracker] can
/// search for [MouseTrackerAnnotation]s at a given position.
/// Usually it is [Layer.findAllAnnotations] of the root layer.
///
/// All of the parameters must be non-null.
MouseTracker(
PointerRouter router,
MouseDetectorAnnotationFinder annotationFinder,
) : super(router, annotationFinder);
} }
...@@ -12,6 +12,7 @@ import 'package:flutter/services.dart'; ...@@ -12,6 +12,7 @@ import 'package:flutter/services.dart';
import 'box.dart'; import 'box.dart';
import 'layer.dart'; import 'layer.dart';
import 'mouse_cursor.dart';
import 'mouse_tracking.dart'; import 'mouse_tracking.dart';
import 'object.dart'; import 'object.dart';
...@@ -759,7 +760,8 @@ class PlatformViewRenderBox extends RenderBox with _PlatformViewGestureMixin { ...@@ -759,7 +760,8 @@ class PlatformViewRenderBox extends RenderBox with _PlatformViewGestureMixin {
context.addLayer(PlatformViewLayer( context.addLayer(PlatformViewLayer(
rect: offset & size, rect: offset & size,
viewId: _controller.viewId, viewId: _controller.viewId,
hoverAnnotation: _hoverAnnotation)); hoverAnnotation: _hoverAnnotation,
));
} }
@override @override
...@@ -787,7 +789,16 @@ mixin _PlatformViewGestureMixin on RenderBox { ...@@ -787,7 +789,16 @@ mixin _PlatformViewGestureMixin on RenderBox {
/// and apply it to all subsequent move events, but there is no down event /// and apply it to all subsequent move events, but there is no down event
/// for a hover. To support native hover gesture handling by platform views, /// for a hover. To support native hover gesture handling by platform views,
/// we attach/detach this layer annotation as necessary. /// we attach/detach this layer annotation as necessary.
MouseTrackerAnnotation _hoverAnnotation; MouseTrackerAnnotation get _hoverAnnotation {
return _cachedHoverAnnotation ??= MouseTrackerAnnotation(
onHover: (PointerHoverEvent event) {
if (_handlePointerEvent != null)
_handlePointerEvent(event);
},
cursor: SystemMouseCursors.uncontrolled,
);
}
MouseTrackerAnnotation _cachedHoverAnnotation;
_HandlePointerEvent _handlePointerEvent; _HandlePointerEvent _handlePointerEvent;
...@@ -830,20 +841,9 @@ mixin _PlatformViewGestureMixin on RenderBox { ...@@ -830,20 +841,9 @@ mixin _PlatformViewGestureMixin on RenderBox {
} }
} }
@override
void attach(PipelineOwner owner) {
super.attach(owner);
assert(_hoverAnnotation == null);
_hoverAnnotation = MouseTrackerAnnotation(onHover: (PointerHoverEvent event) {
if (_handlePointerEvent != null)
_handlePointerEvent(event);
});
}
@override @override
void detach() { void detach() {
_gestureRecognizer.reset(); _gestureRecognizer.reset();
_hoverAnnotation = null;
super.detach(); super.detach();
} }
} }
...@@ -11,12 +11,14 @@ import 'package:flutter/foundation.dart'; ...@@ -11,12 +11,14 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/painting.dart'; import 'package:flutter/painting.dart';
import 'package:flutter/semantics.dart'; import 'package:flutter/semantics.dart';
import 'package:flutter/services.dart';
import 'package:vector_math/vector_math_64.dart'; import 'package:vector_math/vector_math_64.dart';
import 'binding.dart'; import 'binding.dart';
import 'box.dart'; import 'box.dart';
import 'layer.dart'; import 'layer.dart';
import 'mouse_cursor.dart';
import 'mouse_tracking.dart'; import 'mouse_tracking.dart';
import 'object.dart'; import 'object.dart';
...@@ -2676,7 +2678,7 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior { ...@@ -2676,7 +2678,7 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior {
/// ///
/// * [MouseRegion], a widget that listens to hover events using /// * [MouseRegion], a widget that listens to hover events using
/// [RenderMouseRegion]. /// [RenderMouseRegion].
class RenderMouseRegion extends RenderProxyBox { class RenderMouseRegion extends RenderProxyBox implements MouseTrackerAnnotation {
/// 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 /// All parameters are optional. By default this method creates an opaque
...@@ -2685,21 +2687,17 @@ class RenderMouseRegion extends RenderProxyBox { ...@@ -2685,21 +2687,17 @@ class RenderMouseRegion extends RenderProxyBox {
PointerEnterEventListener onEnter, PointerEnterEventListener onEnter,
PointerHoverEventListener onHover, PointerHoverEventListener onHover,
PointerExitEventListener onExit, PointerExitEventListener onExit,
MouseCursor cursor,
bool opaque = true, bool opaque = true,
RenderBox child, RenderBox child,
}) : assert(opaque != null), }) : assert(opaque != null),
_onEnter = onEnter, _onEnter = onEnter,
_onHover = onHover, _onHover = onHover,
_onExit = onExit, _onExit = onExit,
_cursor = cursor,
_opaque = opaque, _opaque = opaque,
_annotationIsActive = false, _annotationIsActive = false,
super(child) { super(child);
_hoverAnnotation = MouseTrackerAnnotation(
onEnter: _handleEnter,
onHover: _handleHover,
onExit: _handleExit,
);
}
/// Whether this object should prevent [RenderMouseRegion]s visually behind it /// Whether this object should prevent [RenderMouseRegion]s visually behind it
/// from detecting the pointer, thus affecting how their [onHover], [onEnter], /// from detecting the pointer, thus affecting how their [onHover], [onEnter],
...@@ -2720,77 +2718,53 @@ class RenderMouseRegion extends RenderProxyBox { ...@@ -2720,77 +2718,53 @@ class RenderMouseRegion extends RenderProxyBox {
set opaque(bool value) { set opaque(bool value) {
if (_opaque != value) { if (_opaque != value) {
_opaque = value; _opaque = value;
// A repaint is needed in order to propagate the new value to
// AnnotatedRegionLayer via [paint].
_markPropertyUpdated(mustRepaint: true); _markPropertyUpdated(mustRepaint: true);
} }
} }
/// Called when a mouse pointer starts being contained by the region (with or @override
/// 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;
PointerEnterEventListener _onEnter;
set onEnter(PointerEnterEventListener value) { set onEnter(PointerEnterEventListener value) {
if (_onEnter != value) { if (_onEnter != value) {
_onEnter = value; _onEnter = value;
_markPropertyUpdated(mustRepaint: false); _markPropertyUpdated(mustRepaint: false);
} }
} }
PointerEnterEventListener _onEnter;
void _handleEnter(PointerEnterEvent event) {
if (_onEnter != null)
_onEnter(event);
}
/// Called when a pointer changes position without buttons pressed and the end @override
/// position is within the region.
PointerHoverEventListener get onHover => _onHover; PointerHoverEventListener get onHover => _onHover;
PointerHoverEventListener _onHover;
set onHover(PointerHoverEventListener value) { set onHover(PointerHoverEventListener value) {
if (_onHover != value) { if (_onHover != value) {
_onHover = value; _onHover = value;
_markPropertyUpdated(mustRepaint: false); _markPropertyUpdated(mustRepaint: false);
} }
} }
PointerHoverEventListener _onHover;
void _handleHover(PointerHoverEvent event) {
if (_onHover != null)
_onHover(event);
}
/// Called when a pointer is no longer contained by the region (with or @override
/// 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;
PointerExitEventListener _onExit;
set onExit(PointerExitEventListener value) { set onExit(PointerExitEventListener value) {
if (_onExit != value) { if (_onExit != value) {
_onExit = value; _onExit = value;
_markPropertyUpdated(mustRepaint: false); _markPropertyUpdated(mustRepaint: false);
} }
} }
PointerExitEventListener _onExit;
void _handleExit(PointerExitEvent event) {
if (_onExit != null)
_onExit(event);
}
// Object used for annotation of the layer used for hover hit detection. @override
MouseTrackerAnnotation _hoverAnnotation; MouseCursor get cursor => _cursor;
MouseCursor _cursor;
/// Object used for annotation of the layer used for hover hit detection. set cursor(MouseCursor value) {
/// if (_cursor != value) {
/// This is only public to allow for testing of Listener widgets. Do not call _cursor = value;
/// in other contexts. // A repaint is needed in order to trigger a device update of
@visibleForTesting // [MouseTracker] so that this new value can be found.
MouseTrackerAnnotation get hoverAnnotation => _hoverAnnotation; _markPropertyUpdated(mustRepaint: true);
}
}
// Call this method when a property has changed and might affect the // Call this method when a property has changed and might affect the
// `_annotationIsActive` bit. // `_annotationIsActive` bit.
...@@ -2807,6 +2781,7 @@ class RenderMouseRegion extends RenderProxyBox { ...@@ -2807,6 +2781,7 @@ class RenderMouseRegion extends RenderProxyBox {
_onEnter != null || _onEnter != null ||
_onHover != null || _onHover != null ||
_onExit != null || _onExit != null ||
_cursor != null ||
opaque opaque
) && RendererBinding.instance.mouseTracker.mouseIsConnected; ) && RendererBinding.instance.mouseTracker.mouseIsConnected;
_setAnnotationIsActive(newAnnotationIsActive); _setAnnotationIsActive(newAnnotationIsActive);
...@@ -2814,6 +2789,7 @@ class RenderMouseRegion extends RenderProxyBox { ...@@ -2814,6 +2789,7 @@ class RenderMouseRegion extends RenderProxyBox {
markNeedsPaint(); markNeedsPaint();
} }
bool _annotationIsActive = false;
void _setAnnotationIsActive(bool value) { void _setAnnotationIsActive(bool value) {
final bool annotationWasActive = _annotationIsActive; final bool annotationWasActive = _annotationIsActive;
_annotationIsActive = value; _annotationIsActive = value;
...@@ -2841,8 +2817,6 @@ class RenderMouseRegion extends RenderProxyBox { ...@@ -2841,8 +2817,6 @@ class RenderMouseRegion extends RenderProxyBox {
super.detach(); super.detach();
} }
bool _annotationIsActive;
@override @override
bool get needsCompositing => super.needsCompositing || _annotationIsActive; bool get needsCompositing => super.needsCompositing || _annotationIsActive;
...@@ -2851,7 +2825,7 @@ class RenderMouseRegion extends RenderProxyBox { ...@@ -2851,7 +2825,7 @@ class RenderMouseRegion extends RenderProxyBox {
if (_annotationIsActive) { if (_annotationIsActive) {
// Annotated region layers are not retained because they do not create engine layers. // Annotated region layers are not retained because they do not create engine layers.
final AnnotatedRegionLayer<MouseTrackerAnnotation> layer = AnnotatedRegionLayer<MouseTrackerAnnotation>( final AnnotatedRegionLayer<MouseTrackerAnnotation> layer = AnnotatedRegionLayer<MouseTrackerAnnotation>(
_hoverAnnotation, this,
size: size, size: size,
offset: offset, offset: offset,
opaque: opaque, opaque: opaque,
...@@ -2879,6 +2853,7 @@ class RenderMouseRegion extends RenderProxyBox { ...@@ -2879,6 +2853,7 @@ class RenderMouseRegion extends RenderProxyBox {
}, },
ifEmpty: '<none>', ifEmpty: '<none>',
)); ));
properties.add(DiagnosticsProperty<MouseCursor>('cursor', cursor, defaultValue: null));
properties.add(DiagnosticsProperty<bool>('opaque', opaque, defaultValue: true)); properties.add(DiagnosticsProperty<bool>('opaque', opaque, defaultValue: true));
} }
} }
......
...@@ -270,4 +270,18 @@ class SystemChannels { ...@@ -270,4 +270,18 @@ class SystemChannels {
'flutter/skia', 'flutter/skia',
JSONMethodCodec(), JSONMethodCodec(),
); );
/// A [MethodChannel] for configuring mouse cursors.
///
/// All outgoing methods defined for this channel uses a `Map<String, dynamic>`
/// to contain multiple parameters, including the following methods (invoked
/// using [OptionalMethodChannel.invokeMethod]):
///
/// * `activateSystemCursor`: Request to set the cursor of a pointer
/// device to a system cursor. The parameters are
/// integer `device`, and string `kind`.
static const MethodChannel mouseCursor = OptionalMethodChannel(
'flutter/mousecursor',
StandardMethodCodec(),
);
} }
...@@ -9,6 +9,7 @@ import 'package:flutter/gestures.dart'; ...@@ -9,6 +9,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'binding.dart';
import 'debug.dart'; import 'debug.dart';
import 'framework.dart'; import 'framework.dart';
import 'localizations.dart'; import 'localizations.dart';
...@@ -5878,17 +5879,21 @@ class _PointerListener extends SingleChildRenderObjectWidget { ...@@ -5878,17 +5879,21 @@ class _PointerListener extends SingleChildRenderObjectWidget {
/// have buttons pressed. /// have buttons pressed.
class MouseRegion extends StatefulWidget { class MouseRegion extends StatefulWidget {
/// Creates a widget that forwards mouse events to callbacks. /// Creates a widget that forwards mouse events to callbacks.
///
/// By default, all callbacks are empty, `cursor` is unset, and `opaque` is
/// `true`.
const MouseRegion({ const MouseRegion({
Key key, Key key,
this.onEnter, this.onEnter,
this.onExit, this.onExit,
this.onHover, this.onHover,
this.cursor,
this.opaque = true, this.opaque = true,
this.child, this.child,
}) : assert(opaque != null), }) : assert(opaque != null),
super(key: key); super(key: key);
/// Called when a mouse pointer has entered this widget. /// Triggered when a mouse pointer has entered this widget.
/// ///
/// This callback is triggered when the pointer, with or without buttons /// This callback is triggered when the pointer, with or without buttons
/// pressed, has started to be contained by the region of this widget. More /// pressed, has started to be contained by the region of this widget. More
...@@ -5916,16 +5921,16 @@ class MouseRegion extends StatefulWidget { ...@@ -5916,16 +5921,16 @@ class MouseRegion extends StatefulWidget {
/// internally implemented. /// internally implemented.
final PointerEnterEventListener onEnter; final PointerEnterEventListener onEnter;
/// Called when a mouse pointer moves within this widget without buttons /// Triggered when a mouse pointer has moved onto or within the widget without
/// pressed. /// buttons pressed.
/// ///
/// This callback is not triggered when the [MouseRegion] has moved /// This callback is not triggered by the movement of an annotation.
/// while being hovered by the mouse pointer.
/// ///
/// {@macro flutter.mouseRegion.triggerTime} /// The time that this callback is triggered is during the callback of a
/// pointer event, which is always between frames.
final PointerHoverEventListener onHover; final PointerHoverEventListener onHover;
/// Called when a mouse pointer has exited this widget when the widget is /// Triggered when a mouse pointer has exited this widget when the widget is
/// still mounted. /// still mounted.
/// ///
/// This callback is triggered when the pointer, with or without buttons /// This callback is triggered when the pointer, with or without buttons
...@@ -6094,9 +6099,22 @@ class MouseRegion extends StatefulWidget { ...@@ -6094,9 +6099,22 @@ class MouseRegion extends StatefulWidget {
/// this callback is internally implemented, but without the restriction. /// this callback is internally implemented, but without the restriction.
final PointerExitEventListener onExit; final PointerExitEventListener onExit;
/// The mouse cursor for mouse pointers that are hovering over the annotated
/// region.
///
/// When a mouse enters the region, its cursor will be changed to the [cursor].
/// The [cursor] defaults to null, meaning the region does not control cursors,
/// but defers the choice to the next region behind this one on the screen in
/// hit-test order, or [SystemMouseCursors.basic] if no others can be found.
/// When the mouse leaves the region, the cursor will be decided by the region
/// found at the new location.
final MouseCursor cursor;
/// Whether this widget should prevent other [MouseRegion]s visually behind it /// Whether this widget should prevent other [MouseRegion]s visually behind it
/// from detecting the pointer, thus affecting how their [onHover], [onEnter], /// from detecting the pointer.
/// and [onExit] behave. ///
/// This changes the list of regions that a pointer hovers, thus affecting how
/// their [onHover], [onEnter], [onExit], and [cursor] behave.
/// ///
/// If [opaque] is true, this widget will absorb the mouse pointer and /// If [opaque] is true, this widget will absorb the mouse pointer and
/// prevent this widget's siblings (or any other widgets that are not /// prevent this widget's siblings (or any other widgets that are not
...@@ -6129,6 +6147,7 @@ class MouseRegion extends StatefulWidget { ...@@ -6129,6 +6147,7 @@ class MouseRegion extends StatefulWidget {
if (onHover != null) if (onHover != null)
listeners.add('hover'); listeners.add('hover');
properties.add(IterableProperty<String>('listeners', listeners, ifEmpty: '<none>')); properties.add(IterableProperty<String>('listeners', listeners, ifEmpty: '<none>'));
properties.add(DiagnosticsProperty<MouseCursor>('cursor', cursor, defaultValue: null));
properties.add(DiagnosticsProperty<bool>('opaque', opaque, defaultValue: true)); properties.add(DiagnosticsProperty<bool>('opaque', opaque, defaultValue: true));
} }
} }
...@@ -6161,6 +6180,7 @@ class _RawMouseRegion extends SingleChildRenderObjectWidget { ...@@ -6161,6 +6180,7 @@ class _RawMouseRegion extends SingleChildRenderObjectWidget {
onEnter: widget.onEnter, onEnter: widget.onEnter,
onHover: widget.onHover, onHover: widget.onHover,
onExit: owner.getHandleExit(), onExit: owner.getHandleExit(),
cursor: widget.cursor,
opaque: widget.opaque, opaque: widget.opaque,
); );
} }
...@@ -6172,6 +6192,7 @@ class _RawMouseRegion extends SingleChildRenderObjectWidget { ...@@ -6172,6 +6192,7 @@ class _RawMouseRegion extends SingleChildRenderObjectWidget {
..onEnter = widget.onEnter ..onEnter = widget.onEnter
..onHover = widget.onHover ..onHover = widget.onHover
..onExit = owner.getHandleExit() ..onExit = owner.getHandleExit()
..cursor = widget.cursor
..opaque = widget.opaque; ..opaque = widget.opaque;
} }
} }
......
// 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 'dart:ui' as ui;
import 'dart:ui' show PointerChange;
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import '../flutter_test_alternative.dart';
typedef MethodCallHandler = Future<dynamic> Function(MethodCall call);
_TestGestureFlutterBinding _binding = _TestGestureFlutterBinding();
void _ensureTestGestureBinding() {
_binding ??= _TestGestureFlutterBinding();
assert(GestureBinding.instance != null);
}
void main() {
MethodCallHandler _methodCallHandler;
// Only one of `logCursors` and `cursorHandler` should be specified.
void _setUpMouseTracker({
MouseDetectorAnnotationFinder annotationFinder,
List<_CursorUpdateDetails> logCursors,
MethodCallHandler cursorHandler,
}) {
assert(logCursors == null || cursorHandler == null);
_methodCallHandler = logCursors != null
? (MethodCall call) async {
logCursors.add(_CursorUpdateDetails.wrap(call));
return;
}
: cursorHandler;
final MouseTracker mouseTracker = MouseTracker(
GestureBinding.instance.pointerRouter,
annotationFinder,
);
RendererBinding.instance.initMouseTracker(mouseTracker);
}
setUp(() {
_ensureTestGestureBinding();
_binding.postFrameCallbacks.clear();
SystemChannels.mouseCursor.setMockMethodCallHandler((MethodCall call) async {
if (_methodCallHandler != null)
return _methodCallHandler(call);
});
});
tearDown(() {
SystemChannels.mouseCursor.setMockMethodCallHandler(null);
});
test('Should work on platforms that does not support mouse cursor', () async {
const MouseTrackerAnnotation annotation = MouseTrackerAnnotation(cursor: SystemMouseCursors.grabbing);
_setUpMouseTracker(
annotationFinder: (Offset position) => <MouseTrackerAnnotation>[annotation],
cursorHandler: (MethodCall call) async {
return null;
},
);
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, const Offset(0.0, 0.0)),
]));
// Passes if no errors are thrown
});
test('pointer is added and removed out of any annotations', () {
final List<_CursorUpdateDetails> logCursors = <_CursorUpdateDetails>[];
MouseTrackerAnnotation annotation;
_setUpMouseTracker(
annotationFinder: (Offset position) => <MouseTrackerAnnotation>[if (annotation != null) annotation],
logCursors: logCursors,
);
// Pointer is added outside of the annotation.
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, const Offset(0.0, 0.0)),
]));
expect(logCursors, <_CursorUpdateDetails>[
_CursorUpdateDetails.activateSystemCursor(device: 0, kind: SystemMouseCursors.basic.kind),
]);
logCursors.clear();
// Pointer moves into the annotation
annotation = const MouseTrackerAnnotation(cursor: SystemMouseCursors.grabbing);
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.hover, const Offset(5.0, 0.0)),
]));
expect(logCursors, <_CursorUpdateDetails>[
_CursorUpdateDetails.activateSystemCursor(device: 0, kind: SystemMouseCursors.grabbing.kind),
]);
logCursors.clear();
// Pointer moves within the annotation
annotation = const MouseTrackerAnnotation(cursor: SystemMouseCursors.grabbing);
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.hover, const Offset(10.0, 0.0)),
]));
expect(logCursors, <_CursorUpdateDetails>[
]);
logCursors.clear();
// Pointer moves out of the annotation
annotation = null;
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.hover, const Offset(0.0, 0.0)),
]));
expect(logCursors, <_CursorUpdateDetails>[
_CursorUpdateDetails.activateSystemCursor(device: 0, kind: SystemMouseCursors.basic.kind),
]);
logCursors.clear();
// Pointer is removed outside of the annotation.
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.remove, const Offset(0.0, 0.0)),
]));
expect(logCursors, const <_CursorUpdateDetails>[
]);
});
test('pointer is added and removed in an annotation', () {
final List<_CursorUpdateDetails> logCursors = <_CursorUpdateDetails>[];
MouseTrackerAnnotation annotation;
_setUpMouseTracker(
annotationFinder: (Offset position) => <MouseTrackerAnnotation>[if (annotation != null) annotation],
logCursors: logCursors,
);
// Pointer is added in the annotation.
annotation = const MouseTrackerAnnotation(cursor: SystemMouseCursors.grabbing);
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, const Offset(0.0, 0.0)),
]));
expect(logCursors, <_CursorUpdateDetails>[
_CursorUpdateDetails.activateSystemCursor(device: 0, kind: SystemMouseCursors.grabbing.kind),
]);
logCursors.clear();
// Pointer moves out of the annotation
annotation = null;
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.hover, const Offset(5.0, 0.0)),
]));
expect(logCursors, <_CursorUpdateDetails>[
_CursorUpdateDetails.activateSystemCursor(device: 0, kind: SystemMouseCursors.basic.kind),
]);
logCursors.clear();
// Pointer moves around out of the annotation
annotation = null;
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.hover, const Offset(10.0, 0.0)),
]));
expect(logCursors, <_CursorUpdateDetails>[
]);
logCursors.clear();
// Pointer moves back into the annotation
annotation = const MouseTrackerAnnotation(cursor: SystemMouseCursors.grabbing);
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.hover, const Offset(0.0, 0.0)),
]));
expect(logCursors, <_CursorUpdateDetails>[
_CursorUpdateDetails.activateSystemCursor(device: 0, kind: SystemMouseCursors.grabbing.kind),
]);
logCursors.clear();
// Pointer is removed within the annotation.
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.remove, const Offset(0.0, 0.0)),
]));
expect(logCursors, <_CursorUpdateDetails>[
]);
});
test('pointer change caused by new frames', () {
final List<_CursorUpdateDetails> logCursors = <_CursorUpdateDetails>[];
MouseTrackerAnnotation annotation;
_setUpMouseTracker(
annotationFinder: (Offset position) => <MouseTrackerAnnotation>[if (annotation != null) annotation],
logCursors: logCursors,
);
// Pointer is added outside of the annotation.
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, const Offset(0.0, 0.0)),
]));
expect(logCursors, <_CursorUpdateDetails>[
_CursorUpdateDetails.activateSystemCursor(device: 0, kind: SystemMouseCursors.basic.kind),
]);
logCursors.clear();
// Synthesize a new frame while changing annotation
annotation = const MouseTrackerAnnotation(cursor: SystemMouseCursors.grabbing);
_binding.scheduleMouseTrackerPostFrameCheck();
_binding.flushPostFrameCallbacks(Duration.zero);
expect(logCursors, <_CursorUpdateDetails>[
_CursorUpdateDetails.activateSystemCursor(device: 0, kind: SystemMouseCursors.grabbing.kind),
]);
logCursors.clear();
// Synthesize a new frame without changing annotation
annotation = const MouseTrackerAnnotation(cursor: SystemMouseCursors.grabbing);
_binding.scheduleMouseTrackerPostFrameCheck();
expect(logCursors, <_CursorUpdateDetails>[
]);
logCursors.clear();
// Pointer is removed outside of the annotation.
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.remove, const Offset(0.0, 0.0)),
]));
expect(logCursors, <_CursorUpdateDetails>[
]);
});
test('The first annotation with non-null cursor is used', () {
final List<_CursorUpdateDetails> logCursors = <_CursorUpdateDetails>[];
List<MouseTrackerAnnotation> annotations;
_setUpMouseTracker(
annotationFinder: (Offset position) sync* { yield* annotations; },
logCursors: logCursors,
);
annotations = <MouseTrackerAnnotation>[
const MouseTrackerAnnotation(cursor: null),
const MouseTrackerAnnotation(cursor: SystemMouseCursors.click),
const MouseTrackerAnnotation(cursor: SystemMouseCursors.grabbing),
];
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, const Offset(0.0, 0.0)),
]));
expect(logCursors, <_CursorUpdateDetails>[
_CursorUpdateDetails.activateSystemCursor(device: 0, kind: SystemMouseCursors.click.kind),
]);
logCursors.clear();
// Remove
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.remove, const Offset(5.0, 0.0)),
]));
});
test('Annotations with null cursors are ignored', () {
final List<_CursorUpdateDetails> logCursors = <_CursorUpdateDetails>[];
List<MouseTrackerAnnotation> annotations;
_setUpMouseTracker(
annotationFinder: (Offset position) sync* { yield* annotations; },
logCursors: logCursors,
);
annotations = <MouseTrackerAnnotation>[
const MouseTrackerAnnotation(cursor: null),
const MouseTrackerAnnotation(cursor: null),
const MouseTrackerAnnotation(cursor: SystemMouseCursors.grabbing),
];
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, const Offset(0.0, 0.0)),
]));
expect(logCursors, <_CursorUpdateDetails>[
_CursorUpdateDetails.activateSystemCursor(device: 0, kind: SystemMouseCursors.grabbing.kind),
]);
logCursors.clear();
// Remove
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.remove, const Offset(5.0, 0.0)),
]));
});
test('Finding no annotation is equivalent to specifying default cursor', () {
final List<_CursorUpdateDetails> logCursors = <_CursorUpdateDetails>[];
MouseTrackerAnnotation annotation;
_setUpMouseTracker(
annotationFinder: (Offset position) => <MouseTrackerAnnotation>[if (annotation != null) annotation],
logCursors: logCursors,
);
// Pointer is added outside of the annotation.
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, const Offset(0.0, 0.0)),
]));
expect(logCursors, <_CursorUpdateDetails>[
_CursorUpdateDetails.activateSystemCursor(device: 0, kind: SystemMouseCursors.basic.kind),
]);
logCursors.clear();
// Pointer moved to an annotation specified with the default cursor
annotation = const MouseTrackerAnnotation(cursor: SystemMouseCursors.basic);
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.hover, const Offset(5.0, 0.0)),
]));
expect(logCursors, <_CursorUpdateDetails>[
]);
logCursors.clear();
// Pointer moved to no annotations
annotation = null;
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.hover, const Offset(0.0, 0.0)),
]));
expect(logCursors, <_CursorUpdateDetails>[
]);
logCursors.clear();
// Remove
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.remove, const Offset(0.0, 0.0)),
]));
});
test('Removing a pointer resets it back to the default cursor', () {
final List<_CursorUpdateDetails> logCursors = <_CursorUpdateDetails>[];
MouseTrackerAnnotation annotation;
_setUpMouseTracker(
annotationFinder: (Offset position) => <MouseTrackerAnnotation>[if (annotation != null) annotation],
logCursors: logCursors,
);
// Pointer is added to the annotation, then removed
annotation = const MouseTrackerAnnotation(cursor: SystemMouseCursors.click);
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, const Offset(0.0, 0.0)),
_pointerData(PointerChange.hover, const Offset(5.0, 0.0)),
_pointerData(PointerChange.remove, const Offset(5.0, 0.0)),
]));
logCursors.clear();
// Pointer is added out of the annotation
annotation = null;
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, const Offset(0.0, 0.0)),
]));
expect(logCursors, <_CursorUpdateDetails>[
_CursorUpdateDetails.activateSystemCursor(device: 0, kind: SystemMouseCursors.basic.kind),
]);
logCursors.clear();
});
test('Pointing devices display cursors separately', () {
final List<_CursorUpdateDetails> logCursors = <_CursorUpdateDetails>[];
_setUpMouseTracker(
annotationFinder: (Offset position) sync* {
if (position.dx > 200) {
yield const MouseTrackerAnnotation(cursor: SystemMouseCursors.forbidden);
} else if (position.dx > 100) {
yield const MouseTrackerAnnotation(cursor: SystemMouseCursors.click);
} else {}
},
logCursors: logCursors,
);
// Pointers are added outside of the annotation.
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, const Offset(0.0, 0.0), device: 1),
_pointerData(PointerChange.add, const Offset(0.0, 0.0), device: 2),
]));
expect(logCursors, <_CursorUpdateDetails>[
_CursorUpdateDetails.activateSystemCursor(device: 1, kind: SystemMouseCursors.basic.kind),
_CursorUpdateDetails.activateSystemCursor(device: 2, kind: SystemMouseCursors.basic.kind),
]);
logCursors.clear();
// Pointer 1 moved to cursor "click"
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.hover, const Offset(101.0, 0.0), device: 1),
]));
expect(logCursors, <_CursorUpdateDetails>[
_CursorUpdateDetails.activateSystemCursor(device: 1, kind: SystemMouseCursors.click.kind),
]);
logCursors.clear();
// Pointer 2 moved to cursor "click"
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.hover, const Offset(102.0, 0.0), device: 2),
]));
expect(logCursors, <_CursorUpdateDetails>[
_CursorUpdateDetails.activateSystemCursor(device: 2, kind: SystemMouseCursors.click.kind),
]);
logCursors.clear();
// Pointer 2 moved to cursor "forbidden"
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.hover, const Offset(202.0, 0.0), device: 2),
]));
expect(logCursors, <_CursorUpdateDetails>[
_CursorUpdateDetails.activateSystemCursor(device: 2, kind: SystemMouseCursors.forbidden.kind),
]);
logCursors.clear();
// Remove
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.remove, const Offset(0.0, 0.0)),
]));
});
}
ui.PointerData _pointerData(
PointerChange change,
Offset logicalPosition, {
int device = 0,
PointerDeviceKind kind = PointerDeviceKind.mouse,
}) {
return ui.PointerData(
change: change,
physicalX: logicalPosition.dx * ui.window.devicePixelRatio,
physicalY: logicalPosition.dy * ui.window.devicePixelRatio,
kind: kind,
device: device,
);
}
class _CursorUpdateDetails extends MethodCall {
const _CursorUpdateDetails(String method, Map<String, dynamic> arguments)
: assert(arguments != null),
super(method, arguments);
_CursorUpdateDetails.wrap(MethodCall call)
: super(call.method, Map<String, dynamic>.from(call.arguments as Map<dynamic, dynamic>));
_CursorUpdateDetails.activateSystemCursor({int device, String kind})
: this('activateSystemCursor', <String, dynamic>{'device': device, 'kind': kind});
@override
Map<String, dynamic> get arguments => super.arguments as Map<String, dynamic>;
@override
bool operator ==(dynamic other) {
if (identical(other, this))
return true;
if (other.runtimeType != runtimeType)
return false;
return other is _CursorUpdateDetails
&& other.method == method
&& other.arguments.length == arguments.length
&& other.arguments.entries.every(
(MapEntry<String, dynamic> entry) =>
arguments.containsKey(entry.key) && arguments[entry.key] == entry.value,
);
}
@override
int get hashCode => hashValues(method, arguments);
@override
String toString() {
return '_CursorUpdateDetails(method: $method, arguments: $arguments)';
}
}
class _TestGestureFlutterBinding extends BindingBase
with SchedulerBinding, ServicesBinding, GestureBinding, SemanticsBinding, RendererBinding {
@override
void initInstances() {
super.initInstances();
postFrameCallbacks = <void Function(Duration)>[];
}
SchedulerPhase _overridePhase;
@override
SchedulerPhase get schedulerPhase => _overridePhase ?? super.schedulerPhase;
// Mannually schedule a postframe check.
//
// In real apps this is done by the renderer binding, but in tests we have to
// bypass the phase assertion of [MouseTracker.schedulePostFrameCheck].
void scheduleMouseTrackerPostFrameCheck() {
final SchedulerPhase lastPhase = _overridePhase;
_overridePhase = SchedulerPhase.persistentCallbacks;
mouseTracker.schedulePostFrameCheck();
_overridePhase = lastPhase;
}
List<void Function(Duration)> postFrameCallbacks;
// Proxy post-frame callbacks.
@override
void addPostFrameCallback(void Function(Duration) callback) {
postFrameCallbacks.add(callback);
}
void flushPostFrameCallbacks(Duration duration) {
for (final void Function(Duration) callback in postFrameCallbacks) {
callback(duration);
}
postFrameCallbacks.clear();
}
}
...@@ -72,13 +72,25 @@ void main() { ...@@ -72,13 +72,25 @@ void main() {
RendererBinding.instance.initMouseTracker(mouseTracker); RendererBinding.instance.initMouseTracker(mouseTracker);
} }
// Set up a trivial test environment that includes one annotation, which adds // Set up a trivial test environment that includes one annotation.
// the enter, hover, and exit events it received to [logEvents]. // This annotation records the enter, hover, and exit events it receives to
// `logEvents`.
// This annotation also contains a cursor with a value of `testCursor`.
// The mouse tracker records the cursor requests it receives to `logCursors`.
MouseTrackerAnnotation _setUpWithOneAnnotation({List<PointerEvent> logEvents}) { MouseTrackerAnnotation _setUpWithOneAnnotation({List<PointerEvent> logEvents}) {
final MouseTrackerAnnotation annotation = MouseTrackerAnnotation( final MouseTrackerAnnotation annotation = MouseTrackerAnnotation(
onEnter: (PointerEnterEvent event) => logEvents.add(event), onEnter: (PointerEnterEvent event) {
onHover: (PointerHoverEvent event) => logEvents.add(event), if (logEvents != null)
onExit: (PointerExitEvent event) => logEvents.add(event), logEvents.add(event);
},
onHover: (PointerHoverEvent event) {
if (logEvents != null)
logEvents.add(event);
},
onExit: (PointerExitEvent event) {
if (logEvents != null)
logEvents.add(event);
},
); );
_setUpMouseAnnotationFinder( _setUpMouseAnnotationFinder(
(Offset position) sync* { (Offset position) sync* {
...@@ -109,6 +121,15 @@ void main() { ...@@ -109,6 +121,15 @@ void main() {
annotation2.toString(), annotation2.toString(),
equals('MouseTrackerAnnotation#${shortHash(annotation2)}(callbacks: <none>)'), equals('MouseTrackerAnnotation#${shortHash(annotation2)}(callbacks: <none>)'),
); );
final MouseTrackerAnnotation annotation3 = MouseTrackerAnnotation(
onEnter: (_) {},
cursor: SystemMouseCursors.grab,
);
expect(
annotation3.toString(),
equals('MouseTrackerAnnotation#${shortHash(annotation3)}(callbacks: [enter], cursor: SystemMouseCursor(grab))'),
);
}); });
test('should detect enter, hover, and exit from Added, Hover, and Removed events', () { test('should detect enter, hover, and exit from Added, Hover, and Removed events', () {
...@@ -126,6 +147,7 @@ void main() { ...@@ -126,6 +147,7 @@ void main() {
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[ ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, const Offset(0.0, 0.0)), _pointerData(PointerChange.add, const Offset(0.0, 0.0)),
])); ]));
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[ expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
const PointerEnterEvent(position: Offset(0.0, 0.0)), const PointerEnterEvent(position: Offset(0.0, 0.0)),
])); ]));
...@@ -141,7 +163,7 @@ void main() { ...@@ -141,7 +163,7 @@ void main() {
const PointerHoverEvent(position: Offset(1.0, 101.0)), const PointerHoverEvent(position: Offset(1.0, 101.0)),
])); ]));
expect(_mouseTracker.mouseIsConnected, isTrue); expect(_mouseTracker.mouseIsConnected, isTrue);
expect(listenerLogs, <bool>[]); expect(listenerLogs, isEmpty);
events.clear(); events.clear();
// Pointer is removed while on the annotation. // Pointer is removed while on the annotation.
...@@ -489,7 +511,6 @@ void main() { ...@@ -489,7 +511,6 @@ void main() {
isInHitRegionOne = false; isInHitRegionOne = false;
isInHitRegionTwo = true; isInHitRegionTwo = true;
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)), _pointerData(PointerChange.hover, const Offset(1.0, 101.0)),
......
...@@ -2,8 +2,10 @@ ...@@ -2,8 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
...@@ -536,6 +538,28 @@ void main() { ...@@ -536,6 +538,28 @@ void main() {
expect(exit2, isEmpty); expect(exit2, isEmpty);
}); });
testWidgets('applies mouse cursor', (WidgetTester tester) async {
await tester.pumpWidget(_Scaffold(
topLeft: MouseRegion(
cursor: SystemMouseCursors.text,
child: Container(width: 10, height: 10),
),
));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: const Offset(100, 100));
addTearDown(gesture.removePointer);
await tester.pump();
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
await gesture.moveTo(const Offset(5, 5));
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
await gesture.moveTo(const Offset(100, 100));
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
});
testWidgets('MouseRegion uses updated callbacks', (WidgetTester tester) async { testWidgets('MouseRegion uses updated callbacks', (WidgetTester tester) async {
final List<String> logs = <String>[]; final List<String> logs = <String>[];
Widget hoverableContainer({ Widget hoverableContainer({
...@@ -1358,7 +1382,7 @@ void main() { ...@@ -1358,7 +1382,7 @@ void main() {
expect(logs, <String>['hover2']); expect(logs, <String>['hover2']);
logs.clear(); logs.clear();
// Compare: It repaints if the MouseRegion is unactivated. // Compare: It repaints if the MouseRegion is deactivated.
await tester.pumpWidget(_Scaffold( await tester.pumpWidget(_Scaffold(
topLeft: Container( topLeft: Container(
height: 10, height: 10,
...@@ -1420,6 +1444,215 @@ void main() { ...@@ -1420,6 +1444,215 @@ void main() {
expect(logs, <String>['paint', 'hover-enter']); expect(logs, <String>['paint', 'hover-enter']);
}); });
testWidgets('Changing MouseRegion.cursor is effective and repaints', (WidgetTester tester) async {
final List<String> logPaints = <String>[];
final List<String> logEnters = <String>[];
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: const Offset(100, 100));
addTearDown(gesture.removePointer);
final VoidCallback onPaintChild = () { logPaints.add('paint'); };
await tester.pumpWidget(_Scaffold(
topLeft: Container(
height: 10,
width: 10,
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
onEnter: (_) { logEnters.add('enter'); },
opaque: true,
child: CustomPaint(painter: _DelegatedPainter(onPaint: onPaintChild)),
),
),
));
await gesture.moveTo(const Offset(5, 5));
expect(logPaints, <String>['paint']);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.forbidden);
expect(logEnters, <String>['enter']);
logPaints.clear();
logEnters.clear();
await tester.pumpWidget(_Scaffold(
topLeft: Container(
height: 10,
width: 10,
child: MouseRegion(
cursor: SystemMouseCursors.text,
onEnter: (_) { logEnters.add('enter'); },
opaque: true,
child: CustomPaint(painter: _DelegatedPainter(onPaint: onPaintChild)),
),
),
));
expect(logPaints, <String>['paint']);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
expect(logEnters, isEmpty);
logPaints.clear();
logEnters.clear();
});
testWidgets('Changing whether MouseRegion.cursor is null is effective and repaints', (WidgetTester tester) async {
final List<String> logEnters = <String>[];
final List<String> logPaints = <String>[];
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: const Offset(100, 100));
addTearDown(gesture.removePointer);
final VoidCallback onPaintChild = () { logPaints.add('paint'); };
await tester.pumpWidget(_Scaffold(
topLeft: Container(
height: 10,
width: 10,
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: MouseRegion(
cursor: SystemMouseCursors.text,
onEnter: (_) { logEnters.add('enter'); },
child: CustomPaint(painter: _DelegatedPainter(onPaint: onPaintChild)),
),
),
),
));
await gesture.moveTo(const Offset(5, 5));
expect(logPaints, <String>['paint']);
expect(logEnters, <String>['enter']);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
logPaints.clear();
logEnters.clear();
await tester.pumpWidget(_Scaffold(
topLeft: Container(
height: 10,
width: 10,
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: MouseRegion(
cursor: null,
onEnter: (_) { logEnters.add('enter'); },
child: CustomPaint(painter: _DelegatedPainter(onPaint: onPaintChild)),
),
),
),
));
expect(logPaints, <String>['paint']);
expect(logEnters, isEmpty);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.forbidden);
logPaints.clear();
logEnters.clear();
await tester.pumpWidget(_Scaffold(
topLeft: Container(
height: 10,
width: 10,
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: MouseRegion(
cursor: SystemMouseCursors.text,
opaque: true,
child: CustomPaint(painter: _DelegatedPainter(onPaint: onPaintChild)),
),
),
),
));
expect(logPaints, <String>['paint']);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
expect(logEnters, isEmpty);
logPaints.clear();
logEnters.clear();
});
testWidgets('Does not trigger side effects during a reparent', (WidgetTester tester) async {
final List<String> logEnters = <String>[];
final List<String> logExits = <String>[];
final List<String> logCursors = <String>[];
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: const Offset(100, 100));
addTearDown(gesture.removePointer);
SystemChannels.mouseCursor.setMockMethodCallHandler((_) async {
logCursors.add('cursor');
});
final GlobalKey key = GlobalKey();
// Pump a row of 2 SizedBox's, each taking 50px of width.
await tester.pumpWidget(_Scaffold(
topLeft: SizedBox(
width: 100,
height: 50,
child: Row(
children: <Widget>[
SizedBox(
width: 50,
height: 50,
child: MouseRegion(
key: key,
onEnter: (_) { logEnters.add('enter'); },
onExit: (_) { logEnters.add('enter'); },
cursor: SystemMouseCursors.click,
),
),
const SizedBox(
width: 50,
height: 50,
),
],
),
),
));
// Move to the mouse region inside the first box.
await gesture.moveTo(const Offset(40, 5));
expect(logEnters, <String>['enter']);
expect(logExits, isEmpty);
expect(logCursors, isNotEmpty);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
logEnters.clear();
logExits.clear();
logCursors.clear();
// Move MouseRegion to the second box while resizing them so that the
// mouse is still on the MouseRegion
await tester.pumpWidget(_Scaffold(
topLeft: SizedBox(
width: 100,
height: 50,
child: Row(
children: <Widget>[
const SizedBox(
width: 30,
height: 50,
),
SizedBox(
width: 70,
height: 50,
child: MouseRegion(
key: key,
onEnter: (_) { logEnters.add('enter'); },
onExit: (_) { logEnters.add('enter'); },
cursor: SystemMouseCursors.click,
),
),
],
),
),
));
expect(logEnters, isEmpty);
expect(logExits, isEmpty);
expect(logCursors, isEmpty);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
});
testWidgets("RenderMouseRegion's debugFillProperties when default", (WidgetTester tester) async { testWidgets("RenderMouseRegion's debugFillProperties when default", (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
RenderMouseRegion().debugFillProperties(builder); RenderMouseRegion().debugFillProperties(builder);
......
...@@ -57,7 +57,7 @@ class TestPointer { ...@@ -57,7 +57,7 @@ class TestPointer {
/// Set when the object is constructed. Defaults to 1. /// Set when the object is constructed. Defaults to 1.
final int pointer; final int pointer;
/// The kind of pointer device to simulate. Defaults to /// The kind of pointing device to simulate. Defaults to
/// [PointerDeviceKind.touch]. /// [PointerDeviceKind.touch].
final PointerDeviceKind kind; final PointerDeviceKind kind;
......
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