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,
......
This diff is collapsed.
...@@ -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';
...@@ -757,9 +758,10 @@ class PlatformViewRenderBox extends RenderBox with _PlatformViewGestureMixin { ...@@ -757,9 +758,10 @@ class PlatformViewRenderBox extends RenderBox with _PlatformViewGestureMixin {
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
assert(_controller.viewId != null); assert(_controller.viewId != null);
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;
} }
} }
......
...@@ -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