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';
export 'src/rendering/layer.dart';
export 'src/rendering/list_body.dart';
export 'src/rendering/list_wheel_viewport.dart';
export 'src/rendering/mouse_cursor.dart';
export 'src/rendering/mouse_tracking.dart';
export 'src/rendering/object.dart';
export 'src/rendering/paragraph.dart';
......
......@@ -142,7 +142,7 @@ class HitTestResult {
_debugVectorMoreOrLessEquals(transform.getColumn(2), Vector4(0, 0, 1, 0)),
'The third row and third column of a transform matrix for pointer '
'events must be Vector4(0, 0, 1, 0) to ensure that a transformed '
'point is directly under the pointer device. Did you forget to run the paint '
'point is directly under the pointing device. Did you forget to run the paint '
'matrix through PointerEvent.removePerspectiveTransform? '
'The provided matrix is:\n$transform'
);
......
......@@ -40,6 +40,7 @@ class RawMaterialButton extends StatefulWidget {
@required this.onPressed,
this.onLongPress,
this.onHighlightChanged,
this.mouseCursor,
this.textStyle,
this.fillColor,
this.focusColor,
......@@ -102,6 +103,11 @@ class RawMaterialButton extends StatefulWidget {
/// [State.setState] is not allowed).
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
/// button's [child].
///
......@@ -401,6 +407,7 @@ class _RawMaterialButtonState extends State<RawMaterialButton> {
onLongPress: widget.onLongPress,
enableFeedback: widget.enableFeedback,
customBorder: effectiveShape,
mouseCursor: widget.mouseCursor,
child: IconTheme.merge(
data: IconThemeData(color: effectiveTextColor),
child: Container(
......
......@@ -144,6 +144,7 @@ class FloatingActionButton extends StatelessWidget {
@required this.onPressed,
this.mini = false,
this.shape,
this.mouseCursor,
this.clipBehavior = Clip.none,
this.focusNode,
this.autofocus = false,
......@@ -183,6 +184,7 @@ class FloatingActionButton extends StatelessWidget {
this.disabledElevation,
@required this.onPressed,
this.shape,
this.mouseCursor,
this.isExtended = true,
this.materialTapTargetSize,
this.clipBehavior = Clip.none,
......@@ -376,6 +378,11 @@ class FloatingActionButton extends StatelessWidget {
/// shape as well.
final ShapeBorder shape;
/// {@macro flutter.material.inkwell.mousecursor}
///
/// Defaults to null.
final MouseCursor mouseCursor;
/// {@macro flutter.widgets.Clip}
///
/// Defaults to [Clip.none], and must not be null.
......@@ -484,6 +491,7 @@ class FloatingActionButton extends StatelessWidget {
Widget result = RawMaterialButton(
onPressed: onPressed,
mouseCursor: mouseCursor,
elevation: elevation,
focusElevation: focusElevation,
hoverElevation: hoverElevation,
......
......@@ -295,6 +295,7 @@ class InkResponse extends StatelessWidget {
this.onLongPress,
this.onHighlightChanged,
this.onHover,
this.mouseCursor,
this.containedInkWell = false,
this.highlightShape = BoxShape.circle,
this.radius,
......@@ -361,6 +362,17 @@ class InkResponse extends StatelessWidget {
/// material.
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.
///
/// This flag also controls whether the splash migrates to the center of the
......@@ -543,6 +555,7 @@ class InkResponse extends StatelessWidget {
onLongPress: onLongPress,
onHighlightChanged: onHighlightChanged,
onHover: onHover,
mouseCursor: mouseCursor,
containedInkWell: containedInkWell,
highlightShape: highlightShape,
radius: radius,
......@@ -590,6 +603,7 @@ class _InnerInkResponse extends StatefulWidget {
this.onLongPress,
this.onHighlightChanged,
this.onHover,
this.mouseCursor,
this.containedInkWell = false,
this.highlightShape = BoxShape.circle,
this.radius,
......@@ -624,6 +638,7 @@ class _InnerInkResponse extends StatefulWidget {
final GestureLongPressCallback onLongPress;
final ValueChanged<bool> onHighlightChanged;
final ValueChanged<bool> onHover;
final MouseCursor mouseCursor;
final bool containedInkWell;
final BoxShape highlightShape;
final double radius;
......@@ -997,6 +1012,7 @@ class _InkResponseState extends State<_InnerInkResponse>
onFocusChange: _handleFocusUpdate,
autofocus: widget.autofocus,
child: MouseRegion(
cursor: widget.mouseCursor,
onEnter: enabled ? _handleMouseEnter : null,
onExit: enabled ? _handleMouseExit : null,
child: GestureDetector(
......@@ -1121,6 +1137,7 @@ class InkWell extends InkResponse {
GestureTapCancelCallback onTapCancel,
ValueChanged<bool> onHighlightChanged,
ValueChanged<bool> onHover,
MouseCursor mouseCursor,
Color focusColor,
Color hoverColor,
Color highlightColor,
......@@ -1145,6 +1162,7 @@ class InkWell extends InkResponse {
onTapCancel: onTapCancel,
onHighlightChanged: onHighlightChanged,
onHover: onHover,
mouseCursor: mouseCursor,
containedInkWell: true,
highlightShape: BoxShape.rectangle,
focusColor: focusColor,
......
This diff is collapsed.
......@@ -12,6 +12,7 @@ import 'package:flutter/services.dart';
import 'box.dart';
import 'layer.dart';
import 'mouse_cursor.dart';
import 'mouse_tracking.dart';
import 'object.dart';
......@@ -757,9 +758,10 @@ class PlatformViewRenderBox extends RenderBox with _PlatformViewGestureMixin {
void paint(PaintingContext context, Offset offset) {
assert(_controller.viewId != null);
context.addLayer(PlatformViewLayer(
rect: offset & size,
viewId: _controller.viewId,
hoverAnnotation: _hoverAnnotation));
rect: offset & size,
viewId: _controller.viewId,
hoverAnnotation: _hoverAnnotation,
));
}
@override
......@@ -787,7 +789,16 @@ mixin _PlatformViewGestureMixin on RenderBox {
/// 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,
/// 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;
......@@ -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
void detach() {
_gestureRecognizer.reset();
_hoverAnnotation = null;
super.detach();
}
}
......@@ -11,12 +11,14 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/painting.dart';
import 'package:flutter/semantics.dart';
import 'package:flutter/services.dart';
import 'package:vector_math/vector_math_64.dart';
import 'binding.dart';
import 'box.dart';
import 'layer.dart';
import 'mouse_cursor.dart';
import 'mouse_tracking.dart';
import 'object.dart';
......@@ -2676,7 +2678,7 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior {
///
/// * [MouseRegion], a widget that listens to hover events using
/// [RenderMouseRegion].
class RenderMouseRegion extends RenderProxyBox {
class RenderMouseRegion extends RenderProxyBox implements MouseTrackerAnnotation {
/// Creates a render object that forwards pointer events to callbacks.
///
/// All parameters are optional. By default this method creates an opaque
......@@ -2685,21 +2687,17 @@ class RenderMouseRegion extends RenderProxyBox {
PointerEnterEventListener onEnter,
PointerHoverEventListener onHover,
PointerExitEventListener onExit,
MouseCursor cursor,
bool opaque = true,
RenderBox child,
}) : assert(opaque != null),
_onEnter = onEnter,
_onHover = onHover,
_onExit = onExit,
_cursor = cursor,
_opaque = opaque,
_annotationIsActive = false,
super(child) {
_hoverAnnotation = MouseTrackerAnnotation(
onEnter: _handleEnter,
onHover: _handleHover,
onExit: _handleExit,
);
}
super(child);
/// Whether this object should prevent [RenderMouseRegion]s visually behind it
/// from detecting the pointer, thus affecting how their [onHover], [onEnter],
......@@ -2720,77 +2718,53 @@ class RenderMouseRegion extends RenderProxyBox {
set opaque(bool value) {
if (_opaque != value) {
_opaque = value;
// A repaint is needed in order to propagate the new value to
// AnnotatedRegionLayer via [paint].
_markPropertyUpdated(mustRepaint: true);
}
}
/// Called when a mouse pointer starts being contained by the region (with or
/// without buttons pressed) for any reason.
///
/// This callback is always matched by a later [onExit].
///
/// See also:
///
/// * [MouseRegion.onEnter], which uses this callback.
@override
PointerEnterEventListener get onEnter => _onEnter;
PointerEnterEventListener _onEnter;
set onEnter(PointerEnterEventListener value) {
if (_onEnter != value) {
_onEnter = value;
_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
/// position is within the region.
@override
PointerHoverEventListener get onHover => _onHover;
PointerHoverEventListener _onHover;
set onHover(PointerHoverEventListener value) {
if (_onHover != value) {
_onHover = value;
_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
/// 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].
@override
PointerExitEventListener get onExit => _onExit;
PointerExitEventListener _onExit;
set onExit(PointerExitEventListener value) {
if (_onExit != value) {
_onExit = value;
_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.
MouseTrackerAnnotation _hoverAnnotation;
/// Object used for annotation of the layer used for hover hit detection.
///
/// This is only public to allow for testing of Listener widgets. Do not call
/// in other contexts.
@visibleForTesting
MouseTrackerAnnotation get hoverAnnotation => _hoverAnnotation;
@override
MouseCursor get cursor => _cursor;
MouseCursor _cursor;
set cursor(MouseCursor value) {
if (_cursor != value) {
_cursor = value;
// A repaint is needed in order to trigger a device update of
// [MouseTracker] so that this new value can be found.
_markPropertyUpdated(mustRepaint: true);
}
}
// Call this method when a property has changed and might affect the
// `_annotationIsActive` bit.
......@@ -2807,6 +2781,7 @@ class RenderMouseRegion extends RenderProxyBox {
_onEnter != null ||
_onHover != null ||
_onExit != null ||
_cursor != null ||
opaque
) && RendererBinding.instance.mouseTracker.mouseIsConnected;
_setAnnotationIsActive(newAnnotationIsActive);
......@@ -2814,6 +2789,7 @@ class RenderMouseRegion extends RenderProxyBox {
markNeedsPaint();
}
bool _annotationIsActive = false;
void _setAnnotationIsActive(bool value) {
final bool annotationWasActive = _annotationIsActive;
_annotationIsActive = value;
......@@ -2841,8 +2817,6 @@ class RenderMouseRegion extends RenderProxyBox {
super.detach();
}
bool _annotationIsActive;
@override
bool get needsCompositing => super.needsCompositing || _annotationIsActive;
......@@ -2851,7 +2825,7 @@ class RenderMouseRegion extends RenderProxyBox {
if (_annotationIsActive) {
// Annotated region layers are not retained because they do not create engine layers.
final AnnotatedRegionLayer<MouseTrackerAnnotation> layer = AnnotatedRegionLayer<MouseTrackerAnnotation>(
_hoverAnnotation,
this,
size: size,
offset: offset,
opaque: opaque,
......@@ -2879,6 +2853,7 @@ class RenderMouseRegion extends RenderProxyBox {
},
ifEmpty: '<none>',
));
properties.add(DiagnosticsProperty<MouseCursor>('cursor', cursor, defaultValue: null));
properties.add(DiagnosticsProperty<bool>('opaque', opaque, defaultValue: true));
}
}
......
......@@ -270,4 +270,18 @@ class SystemChannels {
'flutter/skia',
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';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'binding.dart';
import 'debug.dart';
import 'framework.dart';
import 'localizations.dart';
......@@ -5878,17 +5879,21 @@ class _PointerListener extends SingleChildRenderObjectWidget {
/// have buttons pressed.
class MouseRegion extends StatefulWidget {
/// Creates a widget that forwards mouse events to callbacks.
///
/// By default, all callbacks are empty, `cursor` is unset, and `opaque` is
/// `true`.
const MouseRegion({
Key key,
this.onEnter,
this.onExit,
this.onHover,
this.cursor,
this.opaque = true,
this.child,
}) : assert(opaque != null),
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
/// pressed, has started to be contained by the region of this widget. More
......@@ -5916,16 +5921,16 @@ class MouseRegion extends StatefulWidget {
/// internally implemented.
final PointerEnterEventListener onEnter;
/// Called when a mouse pointer moves within this widget without buttons
/// pressed.
/// Triggered when a mouse pointer has moved onto or within the widget without
/// buttons pressed.
///
/// This callback is not triggered when the [MouseRegion] has moved
/// while being hovered by the mouse pointer.
/// This callback is not triggered by the movement of an annotation.
///
/// {@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;
/// 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.
///
/// This callback is triggered when the pointer, with or without buttons
......@@ -6094,9 +6099,22 @@ class MouseRegion extends StatefulWidget {
/// this callback is internally implemented, but without the restriction.
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
/// from detecting the pointer, thus affecting how their [onHover], [onEnter],
/// and [onExit] behave.
/// from detecting the pointer.
///
/// 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
/// prevent this widget's siblings (or any other widgets that are not
......@@ -6129,6 +6147,7 @@ class MouseRegion extends StatefulWidget {
if (onHover != null)
listeners.add('hover');
properties.add(IterableProperty<String>('listeners', listeners, ifEmpty: '<none>'));
properties.add(DiagnosticsProperty<MouseCursor>('cursor', cursor, defaultValue: null));
properties.add(DiagnosticsProperty<bool>('opaque', opaque, defaultValue: true));
}
}
......@@ -6161,6 +6180,7 @@ class _RawMouseRegion extends SingleChildRenderObjectWidget {
onEnter: widget.onEnter,
onHover: widget.onHover,
onExit: owner.getHandleExit(),
cursor: widget.cursor,
opaque: widget.opaque,
);
}
......@@ -6172,6 +6192,7 @@ class _RawMouseRegion extends SingleChildRenderObjectWidget {
..onEnter = widget.onEnter
..onHover = widget.onHover
..onExit = owner.getHandleExit()
..cursor = widget.cursor
..opaque = widget.opaque;
}
}
......
......@@ -72,13 +72,25 @@ void main() {
RendererBinding.instance.initMouseTracker(mouseTracker);
}
// Set up a trivial test environment that includes one annotation, which adds
// the enter, hover, and exit events it received to [logEvents].
// Set up a trivial test environment that includes one annotation.
// 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}) {
final MouseTrackerAnnotation annotation = MouseTrackerAnnotation(
onEnter: (PointerEnterEvent event) => logEvents.add(event),
onHover: (PointerHoverEvent event) => logEvents.add(event),
onExit: (PointerExitEvent event) => logEvents.add(event),
onEnter: (PointerEnterEvent event) {
if (logEvents != null)
logEvents.add(event);
},
onHover: (PointerHoverEvent event) {
if (logEvents != null)
logEvents.add(event);
},
onExit: (PointerExitEvent event) {
if (logEvents != null)
logEvents.add(event);
},
);
_setUpMouseAnnotationFinder(
(Offset position) sync* {
......@@ -109,6 +121,15 @@ void main() {
annotation2.toString(),
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', () {
......@@ -126,6 +147,7 @@ void main() {
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, const Offset(0.0, 0.0)),
]));
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
const PointerEnterEvent(position: Offset(0.0, 0.0)),
]));
......@@ -141,7 +163,7 @@ void main() {
const PointerHoverEvent(position: Offset(1.0, 101.0)),
]));
expect(_mouseTracker.mouseIsConnected, isTrue);
expect(listenerLogs, <bool>[]);
expect(listenerLogs, isEmpty);
events.clear();
// Pointer is removed while on the annotation.
......@@ -489,7 +511,6 @@ void main() {
isInHitRegionOne = false;
isInHitRegionTwo = true;
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, const Offset(0.0, 101.0)),
_pointerData(PointerChange.hover, const Offset(1.0, 101.0)),
......
......@@ -2,8 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/gestures.dart';
......@@ -536,6 +538,28 @@ void main() {
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 {
final List<String> logs = <String>[];
Widget hoverableContainer({
......@@ -1358,7 +1382,7 @@ void main() {
expect(logs, <String>['hover2']);
logs.clear();
// Compare: It repaints if the MouseRegion is unactivated.
// Compare: It repaints if the MouseRegion is deactivated.
await tester.pumpWidget(_Scaffold(
topLeft: Container(
height: 10,
......@@ -1420,6 +1444,215 @@ void main() {
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 {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
RenderMouseRegion().debugFillProperties(builder);
......
......@@ -57,7 +57,7 @@ class TestPointer {
/// Set when the object is constructed. Defaults to 1.
final int pointer;
/// The kind of pointer device to simulate. Defaults to
/// The kind of pointing device to simulate. Defaults to
/// [PointerDeviceKind.touch].
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