Add mouse cursor API to widgets (phase 1) (#57628)
* Adds default cursor and/or mouseCursor property to a number of widgets.
* Adds `MaterialStateMouseCurrsor`.
diff --git a/packages/flutter/lib/src/material/bottom_navigation_bar.dart b/packages/flutter/lib/src/material/bottom_navigation_bar.dart
index ca3885f..6700a3a 100644
--- a/packages/flutter/lib/src/material/bottom_navigation_bar.dart
+++ b/packages/flutter/lib/src/material/bottom_navigation_bar.dart
@@ -6,6 +6,7 @@
import 'dart:math' as math;
import 'package:flutter/widgets.dart';
+import 'package:flutter/rendering.dart';
import 'package:vector_math/vector_math_64.dart' show Vector3;
import 'bottom_navigation_bar_theme.dart';
@@ -187,6 +188,7 @@
this.unselectedLabelStyle,
this.showSelectedLabels = true,
this.showUnselectedLabels,
+ this.mouseCursor,
}) : assert(items != null),
assert(items.length >= 2),
assert(
@@ -314,6 +316,12 @@
/// Whether the labels are shown for the unselected [BottomNavigationBarItem]s.
final bool showSelectedLabels;
+ /// The cursor for a mouse pointer when it enters or is hovering over the
+ /// tiles.
+ ///
+ /// If this property is null, [SystemMouseCursors.click] will be used.
+ final MouseCursor mouseCursor;
+
@override
_BottomNavigationBarState createState() => _BottomNavigationBarState();
}
@@ -337,12 +345,14 @@
this.showSelectedLabels,
this.showUnselectedLabels,
this.indexLabel,
+ @required this.mouseCursor,
}) : assert(type != null),
assert(item != null),
assert(animation != null),
assert(selected != null),
assert(selectedLabelStyle != null),
- assert(unselectedLabelStyle != null);
+ assert(unselectedLabelStyle != null),
+ assert(mouseCursor != null);
final BottomNavigationBarType type;
final BottomNavigationBarItem item;
@@ -359,6 +369,7 @@
final String indexLabel;
final bool showSelectedLabels;
final bool showUnselectedLabels;
+ final MouseCursor mouseCursor;
@override
Widget build(BuildContext context) {
@@ -452,6 +463,7 @@
children: <Widget>[
InkResponse(
onTap: onTap,
+ mouseCursor: mouseCursor,
child: Padding(
padding: EdgeInsets.only(top: topPadding, bottom: bottomPadding),
child: Column(
@@ -833,6 +845,7 @@
);
break;
}
+ final MouseCursor effectiveMouseCursor = widget.mouseCursor ?? SystemMouseCursors.click;
final List<Widget> tiles = <Widget>[];
for (int i = 0; i < widget.items.length; i++) {
@@ -855,6 +868,7 @@
showSelectedLabels: widget.showSelectedLabels ?? bottomTheme.showSelectedLabels,
showUnselectedLabels: widget.showUnselectedLabels ?? bottomTheme.showUnselectedLabels ?? _defaultShowUnselected,
indexLabel: localizations.tabLabel(tabIndex: i + 1, tabCount: widget.items.length),
+ mouseCursor: effectiveMouseCursor,
));
}
return tiles;
diff --git a/packages/flutter/lib/src/material/button.dart b/packages/flutter/lib/src/material/button.dart
index f8f5677..f052074 100644
--- a/packages/flutter/lib/src/material/button.dart
+++ b/packages/flutter/lib/src/material/button.dart
@@ -103,9 +103,20 @@
/// [State.setState] is not allowed).
final ValueChanged<bool> onHighlightChanged;
- /// {@macro flutter.material.inkwell.mousecursor}
+ /// {@template flutter.material.button.mouseCursor}
+ /// The cursor for a mouse pointer when it enters or is hovering over the
+ /// button.
///
- /// If the property is null, [SystemMouseCursor.click] is used.
+ /// If [mouseCursor] is a [MaterialStateProperty<MouseCursor>],
+ /// [MaterialStateProperty.resolve] is used for the following [MaterialState]s:
+ ///
+ /// * [MaterialState.pressed].
+ /// * [MaterialState.hovered].
+ /// * [MaterialState.focused].
+ /// * [MaterialState.disabled].
+ ///
+ /// If this property is null, [MaterialStateMouseCursor.clickable] will be used.
+ /// {@endtemplate flutter.material.button.mouseCursor}
final MouseCursor mouseCursor;
/// Defines the default text style, with [Material.textStyle], for the
@@ -373,6 +384,10 @@
final ShapeBorder effectiveShape = MaterialStateProperty.resolveAs<ShapeBorder>(widget.shape, _states);
final Offset densityAdjustment = widget.visualDensity.baseSizeAdjustment;
final BoxConstraints effectiveConstraints = widget.visualDensity.effectiveConstraints(widget.constraints);
+ final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor>(
+ widget.mouseCursor ?? MaterialStateMouseCursor.clickable,
+ _states,
+ );
final EdgeInsetsGeometry padding = widget.padding.add(
EdgeInsets.only(
left: densityAdjustment.dx,
@@ -382,6 +397,7 @@
),
).clamp(EdgeInsets.zero, EdgeInsetsGeometry.infinity);
+
final Widget result = ConstrainedBox(
constraints: effectiveConstraints,
child: Material(
@@ -407,7 +423,7 @@
onLongPress: widget.onLongPress,
enableFeedback: widget.enableFeedback,
customBorder: effectiveShape,
- mouseCursor: widget.mouseCursor,
+ mouseCursor: effectiveMouseCursor,
child: IconTheme.merge(
data: IconThemeData(color: effectiveTextColor),
child: Container(
diff --git a/packages/flutter/lib/src/material/checkbox.dart b/packages/flutter/lib/src/material/checkbox.dart
index 30f2fc3..286ac8f 100644
--- a/packages/flutter/lib/src/material/checkbox.dart
+++ b/packages/flutter/lib/src/material/checkbox.dart
@@ -11,6 +11,7 @@
import 'constants.dart';
import 'debug.dart';
+import 'material_state.dart';
import 'theme.dart';
import 'theme_data.dart';
import 'toggleable.dart';
@@ -60,6 +61,7 @@
@required this.value,
this.tristate = false,
@required this.onChanged,
+ this.mouseCursor,
this.activeColor,
this.checkColor,
this.focusColor,
@@ -107,6 +109,23 @@
/// ```
final ValueChanged<bool> onChanged;
+ /// The cursor for a mouse pointer when it enters or is hovering over the
+ /// widget.
+ ///
+ /// If [mouseCursor] is a [MaterialStateProperty<MouseCursor>],
+ /// [MaterialStateProperty.resolve] is used for the following [MaterialState]s:
+ ///
+ /// * [MaterialState.selected].
+ /// * [MaterialState.hovered].
+ /// * [MaterialState.focused].
+ /// * [MaterialState.disabled].
+ ///
+ /// When [value] is null and [tristate] is true, [MaterialState.selected] is
+ /// included as a state.
+ ///
+ /// If this property is null, [MaterialStateMouseCursor.clickable] will be used.
+ final MouseCursor mouseCursor;
+
/// The color to use when this checkbox is checked.
///
/// Defaults to [ThemeData.toggleableActiveColor].
@@ -226,6 +245,16 @@
}
size += (widget.visualDensity ?? themeData.visualDensity).baseSizeAdjustment;
final BoxConstraints additionalConstraints = BoxConstraints.tight(size);
+ final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor>(
+ widget.mouseCursor ?? MaterialStateMouseCursor.clickable,
+ <MaterialState>{
+ if (!enabled) MaterialState.disabled,
+ if (_hovering) MaterialState.hovered,
+ if (_focused) MaterialState.focused,
+ if (widget.tristate || widget.value) MaterialState.selected,
+ },
+ );
+
return FocusableActionDetector(
actions: _actionMap,
focusNode: widget.focusNode,
@@ -233,6 +262,7 @@
enabled: enabled,
onShowFocusHighlight: _handleFocusHighlightChanged,
onShowHoverHighlight: _handleHoverChanged,
+ mouseCursor: effectiveMouseCursor,
child: Builder(
builder: (BuildContext context) {
return _CheckboxRenderObjectWidget(
@@ -309,8 +339,10 @@
@override
void updateRenderObject(BuildContext context, _RenderCheckbox renderObject) {
renderObject
- ..value = value
+ // The `tristate` must be changed before `value` due to the assertion at
+ // the beginning of `set value`.
..tristate = tristate
+ ..value = value
..activeColor = activeColor
..checkColor = checkColor
..inactiveColor = inactiveColor
diff --git a/packages/flutter/lib/src/material/flat_button.dart b/packages/flutter/lib/src/material/flat_button.dart
index 2c580d4..da56989 100644
--- a/packages/flutter/lib/src/material/flat_button.dart
+++ b/packages/flutter/lib/src/material/flat_button.dart
@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
+import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'button.dart';
@@ -104,6 +105,7 @@
@required VoidCallback onPressed,
VoidCallback onLongPress,
ValueChanged<bool> onHighlightChanged,
+ MouseCursor mouseCursor,
ButtonTextTheme textTheme,
Color textColor,
Color disabledTextColor,
@@ -129,6 +131,7 @@
onPressed: onPressed,
onLongPress: onLongPress,
onHighlightChanged: onHighlightChanged,
+ mouseCursor: mouseCursor,
textTheme: textTheme,
textColor: textColor,
disabledTextColor: disabledTextColor,
@@ -161,6 +164,7 @@
@required VoidCallback onPressed,
VoidCallback onLongPress,
ValueChanged<bool> onHighlightChanged,
+ MouseCursor mouseCursor,
ButtonTextTheme textTheme,
Color textColor,
Color disabledTextColor,
@@ -189,6 +193,7 @@
onPressed: onPressed,
onLongPress: onLongPress,
onHighlightChanged: onHighlightChanged,
+ mouseCursor: mouseCursor,
fillColor: buttonTheme.getFillColor(this),
textStyle: theme.textTheme.button.copyWith(color: buttonTheme.getTextColor(this)),
focusColor: buttonTheme.getFocusColor(this),
@@ -224,6 +229,7 @@
@required VoidCallback onPressed,
VoidCallback onLongPress,
ValueChanged<bool> onHighlightChanged,
+ MouseCursor mouseCursor,
ButtonTextTheme textTheme,
Color textColor,
Color disabledTextColor,
@@ -251,6 +257,7 @@
onPressed: onPressed,
onLongPress: onLongPress,
onHighlightChanged: onHighlightChanged,
+ mouseCursor: mouseCursor,
textTheme: textTheme,
textColor: textColor,
disabledTextColor: disabledTextColor,
diff --git a/packages/flutter/lib/src/material/floating_action_button.dart b/packages/flutter/lib/src/material/floating_action_button.dart
index fe6e945..cc3211c 100644
--- a/packages/flutter/lib/src/material/floating_action_button.dart
+++ b/packages/flutter/lib/src/material/floating_action_button.dart
@@ -142,9 +142,9 @@
this.highlightElevation,
this.disabledElevation,
@required this.onPressed,
+ this.mouseCursor,
this.mini = false,
this.shape,
- this.mouseCursor,
this.clipBehavior = Clip.none,
this.focusNode,
this.autofocus = false,
@@ -183,8 +183,8 @@
this.highlightElevation,
this.disabledElevation,
@required this.onPressed,
+ this.mouseCursor = SystemMouseCursors.click,
this.shape,
- this.mouseCursor,
this.isExtended = true,
this.materialTapTargetSize,
this.clipBehavior = Clip.none,
@@ -290,6 +290,9 @@
/// If this is set to null, the button will be disabled.
final VoidCallback onPressed;
+ /// {@macro flutter.material.button.mouseCursor}
+ final MouseCursor mouseCursor;
+
/// The z-coordinate at which to place this button relative to its parent.
///
/// This controls the size of the shadow below the floating action button.
@@ -378,11 +381,6 @@
/// shape as well.
final ShapeBorder shape;
- /// {@macro flutter.material.inkwell.mousecursor}
- ///
- /// If the property is null, [SystemMouseCursor.click] is used.
- final MouseCursor mouseCursor;
-
/// {@macro flutter.widgets.Clip}
///
/// Defaults to [Clip.none], and must not be null.
diff --git a/packages/flutter/lib/src/material/icon_button.dart b/packages/flutter/lib/src/material/icon_button.dart
index 8bec7b3..c30869e 100644
--- a/packages/flutter/lib/src/material/icon_button.dart
+++ b/packages/flutter/lib/src/material/icon_button.dart
@@ -5,6 +5,7 @@
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
+import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'constants.dart';
@@ -151,6 +152,7 @@
this.splashColor,
this.disabledColor,
@required this.onPressed,
+ this.mouseCursor = SystemMouseCursors.click,
this.focusNode,
this.autofocus = false,
this.tooltip,
@@ -274,6 +276,11 @@
/// If this is set to null, the button will be disabled.
final VoidCallback onPressed;
+ /// {@macro flutter.material.inkwell.mousecursor}
+ ///
+ /// Defaults to [SystemMouseCursors.click].
+ final MouseCursor mouseCursor;
+
/// {@macro flutter.widgets.Focus.focusNode}
final FocusNode focusNode;
@@ -370,6 +377,7 @@
autofocus: autofocus,
canRequestFocus: onPressed != null,
onTap: onPressed,
+ mouseCursor: mouseCursor,
enableFeedback: enableFeedback,
child: result,
focusColor: focusColor ?? theme.focusColor,
diff --git a/packages/flutter/lib/src/material/ink_well.dart b/packages/flutter/lib/src/material/ink_well.dart
index 3be189b..9a4f1c0 100644
--- a/packages/flutter/lib/src/material/ink_well.dart
+++ b/packages/flutter/lib/src/material/ink_well.dart
@@ -284,8 +284,8 @@
///
/// Must have an ancestor [Material] widget in which to cause ink reactions.
///
- /// The [containedInkWell], [highlightShape], [enableFeedback], and
- /// [excludeFromSemantics] arguments must not be null.
+ /// The [mouseCursor], [containedInkWell], [highlightShape], [enableFeedback],
+ /// and [excludeFromSemantics] arguments must not be null.
const InkResponse({
Key key,
this.child,
@@ -296,7 +296,7 @@
this.onLongPress,
this.onHighlightChanged,
this.onHover,
- this.mouseCursor,
+ this.mouseCursor = MouseCursor.defer,
this.containedInkWell = false,
this.highlightShape = BoxShape.circle,
this.radius,
@@ -313,7 +313,8 @@
this.canRequestFocus = true,
this.onFocusChange,
this.autofocus = false,
- }) : assert(containedInkWell != null),
+ }) : assert(mouseCursor != null),
+ assert(containedInkWell != null),
assert(highlightShape != null),
assert(enableFeedback != null),
assert(excludeFromSemantics != null),
@@ -363,12 +364,11 @@
/// 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.
- /// {@endtemplate}
+ /// widget.
///
- /// If the property is null, [SystemMouseCursor.click] is used.
+ /// The [cursor] defaults to [MouseCursor.defer], deferring the choice of
+ /// cursor to the next region behing it in hit-test order.
final MouseCursor mouseCursor;
/// Whether this ink response should be clipped its bounds.
@@ -544,7 +544,7 @@
@override
Widget build(BuildContext context) {
final _ParentInkResponseState parentState = _ParentInkResponseProvider.of(context);
- return _InnerInkResponse(
+ return _InkResponseStateWidget(
child: child,
onTap: onTap,
onTapDown: onTapDown,
@@ -553,7 +553,7 @@
onLongPress: onLongPress,
onHighlightChanged: onHighlightChanged,
onHover: onHover,
- mouseCursor: mouseCursor ?? SystemMouseCursors.click,
+ mouseCursor: mouseCursor,
containedInkWell: containedInkWell,
highlightShape: highlightShape,
radius: radius,
@@ -591,8 +591,8 @@
}
}
-class _InnerInkResponse extends StatefulWidget {
- const _InnerInkResponse({
+class _InkResponseStateWidget extends StatefulWidget {
+ const _InkResponseStateWidget({
this.child,
this.onTap,
this.onTapDown,
@@ -601,7 +601,7 @@
this.onLongPress,
this.onHighlightChanged,
this.onHover,
- this.mouseCursor,
+ this.mouseCursor = MouseCursor.defer,
this.containedInkWell = false,
this.highlightShape = BoxShape.circle,
this.radius,
@@ -626,7 +626,8 @@
assert(enableFeedback != null),
assert(excludeFromSemantics != null),
assert(autofocus != null),
- assert(canRequestFocus != null);
+ assert(canRequestFocus != null),
+ assert(mouseCursor != null);
final Widget child;
final GestureTapCallback onTap;
@@ -671,6 +672,7 @@
if (onTapCancel != null) 'tap cancel',
];
properties.add(IterableProperty<String>('gestures', gestures, ifEmpty: '<none>'));
+ properties.add(DiagnosticsProperty<MouseCursor>('mouseCursor', mouseCursor, defaultValue: MouseCursor.defer));
properties.add(DiagnosticsProperty<bool>('containedInkWell', containedInkWell, level: DiagnosticLevel.fine));
properties.add(DiagnosticsProperty<BoxShape>(
'highlightShape',
@@ -689,8 +691,8 @@
focus,
}
-class _InkResponseState extends State<_InnerInkResponse>
- with AutomaticKeepAliveClientMixin<_InnerInkResponse>
+class _InkResponseState extends State<_InkResponseStateWidget>
+ with AutomaticKeepAliveClientMixin<_InkResponseStateWidget>
implements _ParentInkResponseState {
Set<InteractiveInkFeature> _splashes;
InteractiveInkFeature _currentSplash;
@@ -732,7 +734,7 @@
}
@override
- void didUpdateWidget(_InnerInkResponse oldWidget) {
+ void didUpdateWidget(_InkResponseStateWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (_isWidgetEnabled(widget) != _isWidgetEnabled(oldWidget)) {
_handleHoverChange(_hovering);
@@ -988,7 +990,7 @@
super.deactivate();
}
- bool _isWidgetEnabled(_InnerInkResponse widget) {
+ bool _isWidgetEnabled(_InkResponseStateWidget widget) {
return widget.onTap != null || widget.onDoubleTap != null || widget.onLongPress != null;
}
@@ -1146,8 +1148,8 @@
///
/// Must have an ancestor [Material] widget in which to cause ink reactions.
///
- /// The [enableFeedback] and [excludeFromSemantics] arguments must not be
- /// null.
+ /// The [mouseCursor], [enableFeedback], and [excludeFromSemantics] arguments
+ /// must not be null.
const InkWell({
Key key,
Widget child,
@@ -1158,7 +1160,7 @@
GestureTapCancelCallback onTapCancel,
ValueChanged<bool> onHighlightChanged,
ValueChanged<bool> onHover,
- MouseCursor mouseCursor,
+ MouseCursor mouseCursor = MouseCursor.defer,
Color focusColor,
Color hoverColor,
Color highlightColor,
diff --git a/packages/flutter/lib/src/material/list_tile.dart b/packages/flutter/lib/src/material/list_tile.dart
index a423a16..4f4f1b9 100644
--- a/packages/flutter/lib/src/material/list_tile.dart
+++ b/packages/flutter/lib/src/material/list_tile.dart
@@ -13,6 +13,7 @@
import 'debug.dart';
import 'divider.dart';
import 'ink_well.dart';
+import 'material_state.dart';
import 'theme.dart';
import 'theme_data.dart';
@@ -640,6 +641,7 @@
this.enabled = true,
this.onTap,
this.onLongPress,
+ this.mouseCursor,
this.selected = false,
this.focusColor,
this.hoverColor,
@@ -736,6 +738,18 @@
/// Inoperative if [enabled] is false.
final GestureLongPressCallback onLongPress;
+ /// The cursor for a mouse pointer when it enters or is hovering over the
+ /// widget.
+ ///
+ /// If [mouseCursor] is a [MaterialStateProperty<MouseCursor>],
+ /// [MaterialStateProperty.resolve] is used for the following [MaterialState]s:
+ ///
+ /// * [MaterialState.selected].
+ /// * [MaterialState.disabled].
+ ///
+ /// If this property is null, [MaterialStateMouseCursor.clickable] will be used.
+ final MouseCursor mouseCursor;
+
/// If this tile is also [enabled] then icons and text are rendered with the same color.
///
/// By default the selected color is the theme's primary color. The selected color
@@ -909,9 +923,18 @@
?? tileTheme?.contentPadding?.resolve(textDirection)
?? _defaultContentPadding;
+ final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor>(
+ mouseCursor ?? MaterialStateMouseCursor.clickable,
+ <MaterialState>{
+ if (!enabled) MaterialState.disabled,
+ if (selected) MaterialState.selected,
+ },
+ );
+
return InkWell(
onTap: enabled ? onTap : null,
onLongPress: enabled ? onLongPress : null,
+ mouseCursor: effectiveMouseCursor,
canRequestFocus: enabled,
focusNode: focusNode,
focusColor: focusColor,
diff --git a/packages/flutter/lib/src/material/material_button.dart b/packages/flutter/lib/src/material/material_button.dart
index 8bcaf48..feb44c9 100644
--- a/packages/flutter/lib/src/material/material_button.dart
+++ b/packages/flutter/lib/src/material/material_button.dart
@@ -53,6 +53,7 @@
@required this.onPressed,
this.onLongPress,
this.onHighlightChanged,
+ this.mouseCursor,
this.textTheme,
this.textColor,
this.disabledTextColor,
@@ -115,6 +116,9 @@
/// [State.setState] is not allowed).
final ValueChanged<bool> onHighlightChanged;
+ /// {@macro flutter.material.button.mouseCursor}
+ final MouseCursor mouseCursor;
+
/// Defines the button's base colors, and the defaults for the button's minimum
/// size, internal padding, and shape.
///
@@ -387,6 +391,7 @@
onLongPress: onLongPress,
enableFeedback: enableFeedback,
onHighlightChanged: onHighlightChanged,
+ mouseCursor: mouseCursor,
fillColor: buttonTheme.getFillColor(this),
textStyle: theme.textTheme.button.copyWith(color: buttonTheme.getTextColor(this)),
focusColor: focusColor ?? buttonTheme.getFocusColor(this) ?? theme.focusColor,
diff --git a/packages/flutter/lib/src/material/material_state.dart b/packages/flutter/lib/src/material/material_state.dart
index 2d4936e..5d64c89 100644
--- a/packages/flutter/lib/src/material/material_state.dart
+++ b/packages/flutter/lib/src/material/material_state.dart
@@ -4,6 +4,9 @@
import 'dart:ui' show Color;
+import 'package:flutter/foundation.dart';
+import 'package:flutter/rendering.dart';
+
/// Interactive states that some of the Material widgets can take on when
/// receiving input from the user.
///
@@ -178,6 +181,98 @@
Color resolve(Set<MaterialState> states) => _resolve(states);
}
+
+/// Defines a [MouseCursor] whose value depends on a set of [MaterialState]s which
+/// represent the interactive state of a component.
+///
+/// This kind of [MouseCursor] is useful when the set of interactive actions a
+/// widget supports varies with its state. For example, a mouse pointer hovering
+/// over a disabled [FlatButton] should not display [SystemMouseCursors.click],
+/// since the button is not clickable. To solve this, you can use
+/// [MaterialStateMouseCursor] to assign a different cursor (such as
+/// [SystemMouseCursors.basic]) when the [FlatButton] is disabled.
+///
+/// To use a [MaterialStateMouseCursor], you should create a subclass of
+/// [MaterialStateMouseCursor] and implement the abstract `resolve` method.
+///
+/// {@tool snippet}
+///
+/// In this next example, we see how you can create a `MaterialStateMouseCursor` by
+/// extending the abstract class and overriding the `resolve` method.
+///
+/// ```dart
+/// class ButtonCursor extends MaterialStateMouseCursor {
+/// const ButtonCursor();
+///
+/// @override
+/// MouseCursor resolve(Set<MaterialState> states) {
+/// if (states.contains(MaterialState.disabled)) {
+/// return SystemMouseCursors.forbidden;
+/// }
+/// return SystemMouseCursors.click;
+/// }
+///
+/// @override
+/// String get debugDescription => 'ButtonCursor()';
+/// }
+///
+/// class MyFlatButton extends StatelessWidget {
+/// @override
+/// Widget build(BuildContext context) {
+/// return FlatButton(
+/// child: Text('FlatButton'),
+/// onPressed: () {},
+/// mouseCursor: const ButtonCursor(),
+/// );
+/// }
+/// }
+/// ```
+/// {@end-tool}
+///
+/// This should only be used as parameters when they are documented to take
+/// [MaterialStateMouseCursor], otherwise only the default state will be used.
+abstract class MaterialStateMouseCursor extends MouseCursor implements MaterialStateProperty<MouseCursor> {
+ /// Creates a [MaterialStateMouseCursor].
+ const MaterialStateMouseCursor();
+
+ @protected
+ @override
+ MouseCursorSession createSession(int device) {
+ return resolve(<MaterialState>{}).createSession(device);
+ }
+
+ /// Returns a [MouseCursor] that's to be used when a Material component is in
+ /// the specified state.
+ ///
+ /// This method should never return null.
+ @override
+ MouseCursor resolve(Set<MaterialState> states);
+
+ /// A mouse cursor for clickable material widgets, which resolves differently
+ /// when the widget is disabled.
+ ///
+ /// By default this cursor resolves to [SystemMouseCursors.click]. If the widget is
+ /// disabled, the cursor resolves to [SystemMouseCursors.basic].
+ ///
+ /// This cursor is the default for many Material widgets.
+ static const MaterialStateMouseCursor clickable = _ClickableMouseCursor();
+}
+
+class _ClickableMouseCursor extends MaterialStateMouseCursor {
+ const _ClickableMouseCursor();
+
+ @override
+ MouseCursor resolve(Set<MaterialState> states) {
+ if (states.contains(MaterialState.disabled)) {
+ return SystemMouseCursors.basic;
+ }
+ return SystemMouseCursors.click;
+ }
+
+ @override
+ String get debugDescription => 'MaterialStateMouseCursor(clickable)';
+}
+
/// Interface for classes that can return a value of type `T` based on a set of
/// [MaterialState]s.
///
diff --git a/packages/flutter/lib/src/material/outline_button.dart b/packages/flutter/lib/src/material/outline_button.dart
index 2d7fcf0..95de4af 100644
--- a/packages/flutter/lib/src/material/outline_button.dart
+++ b/packages/flutter/lib/src/material/outline_button.dart
@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
+import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'button_theme.dart';
@@ -63,6 +64,7 @@
Key key,
@required VoidCallback onPressed,
VoidCallback onLongPress,
+ MouseCursor mouseCursor,
ButtonTextTheme textTheme,
Color textColor,
Color disabledTextColor,
@@ -89,6 +91,7 @@
key: key,
onPressed: onPressed,
onLongPress: onLongPress,
+ mouseCursor: mouseCursor,
textTheme: textTheme,
textColor: textColor,
disabledTextColor: disabledTextColor,
@@ -119,6 +122,7 @@
Key key,
@required VoidCallback onPressed,
VoidCallback onLongPress,
+ MouseCursor mouseCursor,
ButtonTextTheme textTheme,
Color textColor,
Color disabledTextColor,
@@ -178,6 +182,7 @@
autofocus: autofocus,
onPressed: onPressed,
onLongPress: onLongPress,
+ mouseCursor: mouseCursor,
brightness: buttonTheme.getBrightness(this),
textTheme: textTheme,
textColor: buttonTheme.getTextColor(this),
@@ -218,6 +223,7 @@
Key key,
@required VoidCallback onPressed,
VoidCallback onLongPress,
+ MouseCursor mouseCursor,
ButtonTextTheme textTheme,
Color textColor,
Color disabledTextColor,
@@ -247,6 +253,7 @@
key: key,
onPressed: onPressed,
onLongPress: onLongPress,
+ mouseCursor: mouseCursor,
textTheme: textTheme,
textColor: textColor,
disabledTextColor: disabledTextColor,
@@ -281,6 +288,7 @@
Key key,
@required this.onPressed,
this.onLongPress,
+ this.mouseCursor,
this.brightness,
this.textTheme,
this.textColor,
@@ -309,6 +317,7 @@
final VoidCallback onPressed;
final VoidCallback onLongPress;
+ final MouseCursor mouseCursor;
final Brightness brightness;
final ButtonTextTheme textTheme;
final Color textColor;
@@ -462,6 +471,7 @@
disabledColor: Colors.transparent,
onPressed: widget.onPressed,
onLongPress: widget.onLongPress,
+ mouseCursor: widget.mouseCursor,
elevation: 0.0,
disabledElevation: 0.0,
focusElevation: 0.0,
diff --git a/packages/flutter/lib/src/material/popup_menu.dart b/packages/flutter/lib/src/material/popup_menu.dart
index 955e184..56ba76c 100644
--- a/packages/flutter/lib/src/material/popup_menu.dart
+++ b/packages/flutter/lib/src/material/popup_menu.dart
@@ -17,6 +17,7 @@
import 'list_tile.dart';
import 'material.dart';
import 'material_localizations.dart';
+import 'material_state.dart';
import 'popup_menu_theme.dart';
import 'theme.dart';
import 'tooltip.dart';
@@ -216,6 +217,7 @@
this.enabled = true,
this.height = kMinInteractiveDimension,
this.textStyle,
+ this.mouseCursor,
@required this.child,
}) : assert(enabled != null),
assert(height != null),
@@ -242,6 +244,17 @@
/// If [PopupMenuThemeData.textStyle] is also null, then [ThemeData.textTheme.subtitle1] is used.
final TextStyle textStyle;
+ /// The cursor for a mouse pointer when it enters or is hovering over the
+ /// widget.
+ ///
+ /// If [mouseCursor] is a [MaterialStateProperty<MouseCursor>],
+ /// [MaterialStateProperty.resolve] is used for the following [MaterialState]:
+ ///
+ /// * [MaterialState.disabled].
+ ///
+ /// If this property is null, [MaterialStateMouseCursor.clickable] will be used.
+ final MouseCursor mouseCursor;
+
/// The widget below this widget in the tree.
///
/// Typically a single-line [ListTile] (for menus with icons) or a [Text]. An
@@ -320,10 +333,17 @@
child: item,
);
}
+ final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor>(
+ widget.mouseCursor ?? MaterialStateMouseCursor.clickable,
+ <MaterialState>{
+ if (!widget.enabled) MaterialState.disabled,
+ },
+ );
return InkWell(
onTap: widget.enabled ? handleTap : null,
canRequestFocus: widget.enabled,
+ mouseCursor: effectiveMouseCursor,
child: item,
);
}
diff --git a/packages/flutter/lib/src/material/radio.dart b/packages/flutter/lib/src/material/radio.dart
index dbfe290..42ec911 100644
--- a/packages/flutter/lib/src/material/radio.dart
+++ b/packages/flutter/lib/src/material/radio.dart
@@ -9,6 +9,7 @@
import 'constants.dart';
import 'debug.dart';
+import 'material_state.dart';
import 'theme.dart';
import 'theme_data.dart';
import 'toggleable.dart';
@@ -108,6 +109,7 @@
@required this.value,
@required this.groupValue,
@required this.onChanged,
+ this.mouseCursor,
this.toggleable = false,
this.activeColor,
this.focusColor,
@@ -157,6 +159,20 @@
/// ```
final ValueChanged<T> onChanged;
+ /// The cursor for a mouse pointer when it enters or is hovering over the
+ /// widget.
+ ///
+ /// If [mouseCursor] is a [MaterialStateProperty<MouseCursor>],
+ /// [MaterialStateProperty.resolve] is used for the following [MaterialState]s:
+ ///
+ /// * [MaterialState.selected].
+ /// * [MaterialState.hovered].
+ /// * [MaterialState.focused].
+ /// * [MaterialState.disabled].
+ ///
+ /// If this property is null, [MaterialStateMouseCursor.clickable] will be used.
+ final MouseCursor mouseCursor;
+
/// Set to true if this radio button is allowed to be returned to an
/// indeterminate state by selecting it again when selected.
///
@@ -325,17 +341,29 @@
}
size += (widget.visualDensity ?? themeData.visualDensity).baseSizeAdjustment;
final BoxConstraints additionalConstraints = BoxConstraints.tight(size);
+ final bool selected = widget.value == widget.groupValue;
+ final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor>(
+ widget.mouseCursor ?? MaterialStateMouseCursor.clickable,
+ <MaterialState>{
+ if (!enabled) MaterialState.disabled,
+ if (_hovering) MaterialState.hovered,
+ if (_focused) MaterialState.focused,
+ if (selected) MaterialState.selected,
+ },
+ );
+
return FocusableActionDetector(
actions: _actionMap,
focusNode: widget.focusNode,
autofocus: widget.autofocus,
+ mouseCursor: effectiveMouseCursor,
enabled: enabled,
onShowFocusHighlight: _handleHighlightChanged,
onShowHoverHighlight: _handleHoverChanged,
child: Builder(
builder: (BuildContext context) {
return _RadioRenderObjectWidget(
- selected: widget.value == widget.groupValue,
+ selected: selected,
activeColor: widget.activeColor ?? themeData.toggleableActiveColor,
inactiveColor: _getInactiveColor(themeData),
focusColor: widget.focusColor ?? themeData.focusColor,
diff --git a/packages/flutter/lib/src/material/raised_button.dart b/packages/flutter/lib/src/material/raised_button.dart
index eddd7a3..6373b72 100644
--- a/packages/flutter/lib/src/material/raised_button.dart
+++ b/packages/flutter/lib/src/material/raised_button.dart
@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
+import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'button.dart';
@@ -110,6 +111,7 @@
@required VoidCallback onPressed,
VoidCallback onLongPress,
ValueChanged<bool> onHighlightChanged,
+ MouseCursor mouseCursor,
ButtonTextTheme textTheme,
Color textColor,
Color disabledTextColor,
@@ -146,6 +148,7 @@
onPressed: onPressed,
onLongPress: onLongPress,
onHighlightChanged: onHighlightChanged,
+ mouseCursor: mouseCursor,
textTheme: textTheme,
textColor: textColor,
disabledTextColor: disabledTextColor,
@@ -185,6 +188,7 @@
@required VoidCallback onPressed,
VoidCallback onLongPress,
ValueChanged<bool> onHighlightChanged,
+ MouseCursor mouseCursor,
ButtonTextTheme textTheme,
Color textColor,
Color disabledTextColor,
@@ -217,6 +221,7 @@
onPressed: onPressed,
onLongPress: onLongPress,
onHighlightChanged: onHighlightChanged,
+ mouseCursor: mouseCursor,
clipBehavior: clipBehavior,
fillColor: buttonTheme.getFillColor(this),
textStyle: theme.textTheme.button.copyWith(color: buttonTheme.getTextColor(this)),
@@ -262,6 +267,7 @@
@required VoidCallback onPressed,
VoidCallback onLongPress,
ValueChanged<bool> onHighlightChanged,
+ MouseCursor mouseCursor,
ButtonTextTheme textTheme,
Color textColor,
Color disabledTextColor,
@@ -296,6 +302,7 @@
onPressed: onPressed,
onLongPress: onLongPress,
onHighlightChanged: onHighlightChanged,
+ mouseCursor: mouseCursor,
textTheme: textTheme,
textColor: textColor,
disabledTextColor: disabledTextColor,
diff --git a/packages/flutter/lib/src/material/slider.dart b/packages/flutter/lib/src/material/slider.dart
index 4498934..96ca44b 100644
--- a/packages/flutter/lib/src/material/slider.dart
+++ b/packages/flutter/lib/src/material/slider.dart
@@ -17,6 +17,7 @@
import 'constants.dart';
import 'debug.dart';
import 'material.dart';
+import 'material_state.dart';
import 'slider_theme.dart';
import 'theme.dart';
@@ -159,6 +160,7 @@
this.label,
this.activeColor,
this.inactiveColor,
+ this.mouseCursor,
this.semanticFormatterCallback,
this.focusNode,
this.autofocus = false,
@@ -188,6 +190,7 @@
this.max = 1.0,
this.divisions,
this.label,
+ this.mouseCursor,
this.activeColor,
this.inactiveColor,
this.semanticFormatterCallback,
@@ -381,6 +384,19 @@
/// Ignored if this slider is created with [Slider.adaptive].
final Color inactiveColor;
+ /// The cursor for a mouse pointer when it enters or is hovering over the
+ /// widget.
+ ///
+ /// If [mouseCursor] is a [MaterialStateProperty<MouseCursor>],
+ /// [MaterialStateProperty.resolve] is used for the following [MaterialState]s:
+ ///
+ /// * [MaterialState.hovered].
+ /// * [MaterialState.focused].
+ /// * [MaterialState.disabled].
+ ///
+ /// If this property is null, [MaterialStateMouseCursor.clickable] will be used.
+ final MouseCursor mouseCursor;
+
/// The callback used to create a semantic value from a slider value.
///
/// Defaults to formatting values as a percentage.
@@ -537,7 +553,7 @@
widget.onChangeEnd(_lerp(value));
}
- void _actionHandler (_AdjustSliderIntent intent) {
+ void _actionHandler(_AdjustSliderIntent intent) {
final _RenderSlider renderSlider = _renderObjectKey.currentContext.findRenderObject() as _RenderSlider;
final TextDirection textDirection = Directionality.of(_renderObjectKey.currentContext);
switch (intent.type) {
@@ -682,6 +698,14 @@
color: theme.colorScheme.onPrimary,
),
);
+ final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor>(
+ widget.mouseCursor ?? MaterialStateMouseCursor.clickable,
+ <MaterialState>{
+ if (!_enabled) MaterialState.disabled,
+ if (_hovering) MaterialState.hovered,
+ if (_focused) MaterialState.focused,
+ },
+ );
// This size is used as the max bounds for the painting of the value
// indicators It must be kept in sync with the function with the same name
@@ -696,6 +720,7 @@
enabled: _enabled,
onShowFocusHighlight: _handleFocusHighlightChanged,
onShowHoverHighlight: _handleHoverChanged,
+ mouseCursor: effectiveMouseCursor,
child: CompositedTransformTarget(
link: _layerLink,
child: _SliderRenderObjectWidget(
diff --git a/packages/flutter/lib/src/material/switch.dart b/packages/flutter/lib/src/material/switch.dart
index 86698c6..1bf29a8 100644
--- a/packages/flutter/lib/src/material/switch.dart
+++ b/packages/flutter/lib/src/material/switch.dart
@@ -11,6 +11,7 @@
import 'colors.dart';
import 'constants.dart';
import 'debug.dart';
+import 'material_state.dart';
import 'shadows.dart';
import 'theme.dart';
import 'theme_data.dart';
@@ -76,6 +77,7 @@
this.onInactiveThumbImageError,
this.materialTapTargetSize,
this.dragStartBehavior = DragStartBehavior.start,
+ this.mouseCursor,
this.focusColor,
this.hoverColor,
this.focusNode,
@@ -109,6 +111,7 @@
this.onInactiveThumbImageError,
this.materialTapTargetSize,
this.dragStartBehavior = DragStartBehavior.start,
+ this.mouseCursor,
this.focusColor,
this.hoverColor,
this.focusNode,
@@ -206,6 +209,20 @@
/// {@macro flutter.cupertino.switch.dragStartBehavior}
final DragStartBehavior dragStartBehavior;
+ /// The cursor for a mouse pointer when it enters or is hovering over the
+ /// widget.
+ ///
+ /// If [mouseCursor] is a [MaterialStateProperty<MouseCursor>],
+ /// [MaterialStateProperty.resolve] is used for the following [MaterialState]s:
+ ///
+ /// * [MaterialState.selected].
+ /// * [MaterialState.hovered].
+ /// * [MaterialState.focused].
+ /// * [MaterialState.disabled].
+ ///
+ /// If this property is null, [MaterialStateMouseCursor.clickable] will be used.
+ final MouseCursor mouseCursor;
+
/// The color for the button's [Material] when it has the input focus.
final Color focusColor;
@@ -303,6 +320,15 @@
inactiveThumbColor = widget.inactiveThumbColor ?? (isDark ? Colors.grey.shade800 : Colors.grey.shade400);
inactiveTrackColor = widget.inactiveTrackColor ?? (isDark ? Colors.white10 : Colors.black12);
}
+ final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor>(
+ widget.mouseCursor ?? MaterialStateMouseCursor.clickable,
+ <MaterialState>{
+ if (!enabled) MaterialState.disabled,
+ if (_hovering) MaterialState.hovered,
+ if (_focused) MaterialState.focused,
+ if (widget.value) MaterialState.selected,
+ },
+ );
return FocusableActionDetector(
actions: _actionMap,
@@ -311,6 +337,7 @@
enabled: enabled,
onShowFocusHighlight: _handleFocusHighlightChanged,
onShowHoverHighlight: _handleHoverChanged,
+ mouseCursor: effectiveMouseCursor,
child: Builder(
builder: (BuildContext context) {
return _SwitchRenderObjectWidget(
diff --git a/packages/flutter/lib/src/material/tabs.dart b/packages/flutter/lib/src/material/tabs.dart
index d9cf9c6..248e82e 100644
--- a/packages/flutter/lib/src/material/tabs.dart
+++ b/packages/flutter/lib/src/material/tabs.dart
@@ -611,6 +611,7 @@
this.unselectedLabelColor,
this.unselectedLabelStyle,
this.dragStartBehavior = DragStartBehavior.start,
+ this.mouseCursor,
this.onTap,
}) : assert(tabs != null),
assert(isScrollable != null),
@@ -734,6 +735,12 @@
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
final DragStartBehavior dragStartBehavior;
+ /// The cursor for a mouse pointer when it enters or is hovering over the
+ /// individual tab widgets.
+ ///
+ /// If this property is null, [SystemMouseCursors.click] will be used.
+ final MouseCursor mouseCursor;
+
/// An optional callback that's called when the [TabBar] is tapped.
///
/// The callback is applied to the index of the tab where the tap occurred.
@@ -1067,6 +1074,7 @@
final int tabCount = widget.tabs.length;
for (int index = 0; index < tabCount; index += 1) {
wrappedTabs[index] = InkWell(
+ mouseCursor: widget.mouseCursor ?? SystemMouseCursors.click,
onTap: () { _handleTap(index); },
child: Padding(
padding: EdgeInsets.only(bottom: widget.indicatorWeight),
diff --git a/packages/flutter/lib/src/material/text_field.dart b/packages/flutter/lib/src/material/text_field.dart
index f50457c..80e756b 100644
--- a/packages/flutter/lib/src/material/text_field.dart
+++ b/packages/flutter/lib/src/material/text_field.dart
@@ -1087,6 +1087,7 @@
onSelectionHandleTapped: _handleSelectionHandleTapped,
inputFormatters: formatters,
rendererIgnoresPointer: true,
+ mouseCursor: MouseCursor.defer, // TextField will handle the cursor
cursorWidth: widget.cursorWidth,
cursorRadius: cursorRadius,
cursorColor: cursorColor,
@@ -1129,6 +1130,7 @@
return IgnorePointer(
ignoring: !_isEnabled,
child: MouseRegion(
+ cursor: SystemMouseCursors.text,
onEnter: (PointerEnterEvent event) => _handleHover(true),
onExit: (PointerExitEvent event) => _handleHover(false),
child: AnimatedBuilder(
diff --git a/packages/flutter/lib/src/material/toggle_buttons.dart b/packages/flutter/lib/src/material/toggle_buttons.dart
index 0186136..4d3f0a2 100644
--- a/packages/flutter/lib/src/material/toggle_buttons.dart
+++ b/packages/flutter/lib/src/material/toggle_buttons.dart
@@ -166,6 +166,7 @@
@required this.children,
@required this.isSelected,
this.onPressed,
+ this.mouseCursor,
this.textStyle,
this.constraints,
this.color,
@@ -218,6 +219,9 @@
/// When the callback is null, all toggle buttons will be disabled.
final void Function(int index) onPressed;
+ /// {@macro flutter.material.button.mouseCursor}
+ final MouseCursor mouseCursor;
+
/// The [TextStyle] to apply to any text in these toggle buttons.
///
/// [TextStyle.color] will be ignored and substituted by [color],
@@ -601,6 +605,7 @@
onPressed: onPressed != null
? () { onPressed(index); }
: null,
+ mouseCursor: mouseCursor,
leadingBorderSide: leadingBorderSide,
horizontalBorderSide: horizontalBorderSide,
trailingBorderSide: trailingBorderSide,
@@ -667,6 +672,7 @@
this.splashColor,
this.focusNode,
this.onPressed,
+ this.mouseCursor,
this.leadingBorderSide,
this.horizontalBorderSide,
this.trailingBorderSide,
@@ -726,6 +732,9 @@
/// If this is null, the button will be disabled, see [enabled].
final VoidCallback onPressed;
+ /// {@macro flutter.material.button.mouseCursor}
+ final MouseCursor mouseCursor;
+
/// The width and color of the button's leading side border.
final BorderSide leadingBorderSide;
@@ -821,6 +830,7 @@
focusNode: focusNode,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
onPressed: onPressed,
+ mouseCursor: mouseCursor,
child: child,
),
);
diff --git a/packages/flutter/lib/src/widgets/actions.dart b/packages/flutter/lib/src/widgets/actions.dart
index d4a52b8..4c2dd95 100644
--- a/packages/flutter/lib/src/widgets/actions.dart
+++ b/packages/flutter/lib/src/widgets/actions.dart
@@ -5,6 +5,7 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/gestures.dart';
+import 'package:flutter/rendering.dart';
import 'basic.dart';
import 'focus_manager.dart';
@@ -869,7 +870,7 @@
class FocusableActionDetector extends StatefulWidget {
/// Create a const [FocusableActionDetector].
///
- /// The [enabled], [autofocus], and [child] arguments must not be null.
+ /// The [enabled], [autofocus], [mouseCursor], and [child] arguments must not be null.
const FocusableActionDetector({
Key key,
this.enabled = true,
@@ -880,9 +881,11 @@
this.onShowFocusHighlight,
this.onShowHoverHighlight,
this.onFocusChange,
+ this.mouseCursor = MouseCursor.defer,
@required this.child,
}) : assert(enabled != null),
assert(autofocus != null),
+ assert(mouseCursor != null),
assert(child != null),
super(key: key);
@@ -923,6 +926,13 @@
/// Called with true if the [focusNode] has primary focus.
final ValueChanged<bool> onFocusChange;
+ /// The cursor for a mouse pointer when it enters or is hovering over the
+ /// widget.
+ ///
+ /// The [cursor] defaults to [MouseCursor.defer], deferring the choice of
+ /// cursor to the next region behing it in hit-test order.
+ final MouseCursor mouseCursor;
+
/// The child widget for this [FocusableActionDetector] widget.
///
/// {@macro flutter.widgets.child}
@@ -1073,6 +1083,7 @@
Widget child = MouseRegion(
onEnter: _handleMouseEnter,
onExit: _handleMouseExit,
+ cursor: widget.mouseCursor,
child: Focus(
focusNode: widget.focusNode,
autofocus: widget.autofocus,
diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart
index 081a371..ff29484 100644
--- a/packages/flutter/lib/src/widgets/editable_text.dart
+++ b/packages/flutter/lib/src/widgets/editable_text.dart
@@ -390,6 +390,7 @@
this.onSelectionChanged,
this.onSelectionHandleTapped,
List<TextInputFormatter> inputFormatters,
+ this.mouseCursor,
this.rendererIgnoresPointer = false,
this.cursorWidth = 2.0,
this.cursorRadius,
@@ -979,6 +980,16 @@
/// {@endtemplate}
final List<TextInputFormatter> inputFormatters;
+ /// The cursor for a mouse pointer when it enters or is hovering over the
+ /// widget.
+ ///
+ /// If this property is null, [SystemMouseCursors.text] will be used.
+ ///
+ /// The [mouseCursor] is the only property of [EditableText] that controls the
+ /// mouse pointer. All other properties related to "cursor" stands for the text
+ /// cursor, which is usually a blinking vertical line at the editing position.
+ final MouseCursor mouseCursor;
+
/// If true, the [RenderEditable] created by this widget will not handle
/// pointer events, see [renderEditable] and [RenderEditable.ignorePointer].
///
@@ -2018,68 +2029,71 @@
super.build(context); // See AutomaticKeepAliveClientMixin.
final TextSelectionControls controls = widget.selectionControls;
- return Scrollable(
- excludeFromSemantics: true,
- axisDirection: _isMultiline ? AxisDirection.down : AxisDirection.right,
- controller: _scrollController,
- physics: widget.scrollPhysics,
- dragStartBehavior: widget.dragStartBehavior,
- viewportBuilder: (BuildContext context, ViewportOffset offset) {
- return CompositedTransformTarget(
- link: _toolbarLayerLink,
- child: Semantics(
- onCopy: _semanticsOnCopy(controls),
- onCut: _semanticsOnCut(controls),
- onPaste: _semanticsOnPaste(controls),
- child: _Editable(
- key: _editableKey,
- startHandleLayerLink: _startHandleLayerLink,
- endHandleLayerLink: _endHandleLayerLink,
- textSpan: buildTextSpan(),
- value: _value,
- cursorColor: _cursorColor,
- backgroundCursorColor: widget.backgroundCursorColor,
- showCursor: EditableText.debugDeterministicCursor
- ? ValueNotifier<bool>(widget.showCursor)
- : _cursorVisibilityNotifier,
- forceLine: widget.forceLine,
- readOnly: widget.readOnly,
- hasFocus: _hasFocus,
- maxLines: widget.maxLines,
- minLines: widget.minLines,
- expands: widget.expands,
- strutStyle: widget.strutStyle,
- selectionColor: widget.selectionColor,
- textScaleFactor: widget.textScaleFactor ?? MediaQuery.textScaleFactorOf(context),
- textAlign: widget.textAlign,
- textDirection: _textDirection,
- locale: widget.locale,
- textWidthBasis: widget.textWidthBasis,
- obscuringCharacter: widget.obscuringCharacter,
- obscureText: widget.obscureText,
- autocorrect: widget.autocorrect,
- smartDashesType: widget.smartDashesType,
- smartQuotesType: widget.smartQuotesType,
- enableSuggestions: widget.enableSuggestions,
- offset: offset,
- onSelectionChanged: _handleSelectionChanged,
- onCaretChanged: _handleCaretChanged,
- rendererIgnoresPointer: widget.rendererIgnoresPointer,
- cursorWidth: widget.cursorWidth,
- cursorRadius: widget.cursorRadius,
- cursorOffset: widget.cursorOffset,
- selectionHeightStyle: widget.selectionHeightStyle,
- selectionWidthStyle: widget.selectionWidthStyle,
- paintCursorAboveText: widget.paintCursorAboveText,
- enableInteractiveSelection: widget.enableInteractiveSelection,
- textSelectionDelegate: this,
- devicePixelRatio: _devicePixelRatio,
- promptRectRange: _currentPromptRectRange,
- promptRectColor: widget.autocorrectionTextRectColor,
+ return MouseRegion(
+ cursor: widget.mouseCursor ?? SystemMouseCursors.text,
+ child: Scrollable(
+ excludeFromSemantics: true,
+ axisDirection: _isMultiline ? AxisDirection.down : AxisDirection.right,
+ controller: _scrollController,
+ physics: widget.scrollPhysics,
+ dragStartBehavior: widget.dragStartBehavior,
+ viewportBuilder: (BuildContext context, ViewportOffset offset) {
+ return CompositedTransformTarget(
+ link: _toolbarLayerLink,
+ child: Semantics(
+ onCopy: _semanticsOnCopy(controls),
+ onCut: _semanticsOnCut(controls),
+ onPaste: _semanticsOnPaste(controls),
+ child: _Editable(
+ key: _editableKey,
+ startHandleLayerLink: _startHandleLayerLink,
+ endHandleLayerLink: _endHandleLayerLink,
+ textSpan: buildTextSpan(),
+ value: _value,
+ cursorColor: _cursorColor,
+ backgroundCursorColor: widget.backgroundCursorColor,
+ showCursor: EditableText.debugDeterministicCursor
+ ? ValueNotifier<bool>(widget.showCursor)
+ : _cursorVisibilityNotifier,
+ forceLine: widget.forceLine,
+ readOnly: widget.readOnly,
+ hasFocus: _hasFocus,
+ maxLines: widget.maxLines,
+ minLines: widget.minLines,
+ expands: widget.expands,
+ strutStyle: widget.strutStyle,
+ selectionColor: widget.selectionColor,
+ textScaleFactor: widget.textScaleFactor ?? MediaQuery.textScaleFactorOf(context),
+ textAlign: widget.textAlign,
+ textDirection: _textDirection,
+ locale: widget.locale,
+ textWidthBasis: widget.textWidthBasis,
+ obscuringCharacter: widget.obscuringCharacter,
+ obscureText: widget.obscureText,
+ autocorrect: widget.autocorrect,
+ smartDashesType: widget.smartDashesType,
+ smartQuotesType: widget.smartQuotesType,
+ enableSuggestions: widget.enableSuggestions,
+ offset: offset,
+ onSelectionChanged: _handleSelectionChanged,
+ onCaretChanged: _handleCaretChanged,
+ rendererIgnoresPointer: widget.rendererIgnoresPointer,
+ cursorWidth: widget.cursorWidth,
+ cursorRadius: widget.cursorRadius,
+ cursorOffset: widget.cursorOffset,
+ selectionHeightStyle: widget.selectionHeightStyle,
+ selectionWidthStyle: widget.selectionWidthStyle,
+ paintCursorAboveText: widget.paintCursorAboveText,
+ enableInteractiveSelection: widget.enableInteractiveSelection,
+ textSelectionDelegate: this,
+ devicePixelRatio: _devicePixelRatio,
+ promptRectRange: _currentPromptRectRange,
+ promptRectColor: widget.autocorrectionTextRectColor,
+ ),
),
- ),
- );
- },
+ );
+ },
+ ),
);
}
diff --git a/packages/flutter/lib/src/widgets/modal_barrier.dart b/packages/flutter/lib/src/widgets/modal_barrier.dart
index 24e0002..b83aa18 100644
--- a/packages/flutter/lib/src/widgets/modal_barrier.dart
+++ b/packages/flutter/lib/src/widgets/modal_barrier.dart
@@ -4,6 +4,7 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
+import 'package:flutter/rendering.dart';
import 'basic.dart';
import 'container.dart';
@@ -105,6 +106,7 @@
label: semanticsDismissible ? semanticsLabel : null,
textDirection: semanticsDismissible && semanticsLabel != null ? Directionality.of(context) : null,
child: MouseRegion(
+ cursor: SystemMouseCursors.basic,
opaque: true,
child: ConstrainedBox(
constraints: const BoxConstraints.expand(),
diff --git a/packages/flutter/test/material/bottom_navigation_bar_test.dart b/packages/flutter/test/material/bottom_navigation_bar_test.dart
index 793611e..d4ff1cf 100644
--- a/packages/flutter/test/material/bottom_navigation_bar_test.dart
+++ b/packages/flutter/test/material/bottom_navigation_bar_test.dart
@@ -1661,6 +1661,52 @@
semantics.dispose();
});
+ testWidgets('BottomNavigationBar changes mouse cursor when the tile is hovered over', (WidgetTester tester) async {
+ // Test BottomNavigationBar() constructor
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ bottomNavigationBar: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: BottomNavigationBar(
+ mouseCursor: SystemMouseCursors.text,
+ items: const <BottomNavigationBarItem>[
+ BottomNavigationBarItem(icon: Icon(Icons.ac_unit), title: Text('AC')),
+ BottomNavigationBarItem(icon: Icon(Icons.access_alarm), title: Text('Alarm')),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+
+ final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
+ await gesture.addPointer(location: tester.getCenter(find.text('AC')));
+ addTearDown(gesture.removePointer);
+
+ await tester.pump();
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
+
+ // Test default cursor
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ bottomNavigationBar: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: BottomNavigationBar(
+ items: const <BottomNavigationBarItem>[
+ BottomNavigationBarItem(icon: Icon(Icons.ac_unit), title: Text('AC')),
+ BottomNavigationBarItem(icon: Icon(Icons.access_alarm), title: Text('Alarm')),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
+ });
}
Widget boilerplate({ Widget bottomNavigationBar, @required TextDirection textDirection }) {
diff --git a/packages/flutter/test/material/checkbox_test.dart b/packages/flutter/test/material/checkbox_test.dart
index 3b60092..33b3f63 100644
--- a/packages/flutter/test/material/checkbox_test.dart
+++ b/packages/flutter/test/material/checkbox_test.dart
@@ -602,4 +602,120 @@
await tester.pumpAndSettle();
expect(box.size, equals(const Size(60, 36)));
});
+
+ testWidgets('Checkbox changes mouse cursor when hovered', (WidgetTester tester) async {
+ // Test Checkbox() constructor
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: Align(
+ alignment: Alignment.topLeft,
+ child: Material(
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: Checkbox(
+ mouseCursor: SystemMouseCursors.text,
+ value: true,
+ onChanged: (_) {},
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
+ await gesture.addPointer(location: tester.getCenter(find.byType(Checkbox)));
+ addTearDown(gesture.removePointer);
+
+ await tester.pump();
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
+
+ // Test default cursor
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: Align(
+ alignment: Alignment.topLeft,
+ child: Material(
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: Checkbox(
+ value: true,
+ onChanged: (_) {},
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
+
+ // Test default cursor when disabled
+ await tester.pumpWidget(
+ const MaterialApp(
+ home: Scaffold(
+ body: Align(
+ alignment: Alignment.topLeft,
+ child: Material(
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: Checkbox(
+ value: true,
+ onChanged: null,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
+
+ // Test cursor when tristate
+ await tester.pumpWidget(
+ const MaterialApp(
+ home: Scaffold(
+ body: Align(
+ alignment: Alignment.topLeft,
+ child: Material(
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: Checkbox(
+ value: null,
+ tristate: true,
+ onChanged: null,
+ mouseCursor: _SelectedGrabMouseCursor(),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.grab);
+
+ await tester.pumpAndSettle();
+ });
+}
+
+class _SelectedGrabMouseCursor extends MaterialStateMouseCursor {
+ const _SelectedGrabMouseCursor();
+
+ @override
+ MouseCursor resolve(Set<MaterialState> states) {
+ if (states.contains(MaterialState.selected)) {
+ return SystemMouseCursors.grab;
+ }
+ return SystemMouseCursors.basic;
+ }
+
+ @override
+ String get debugDescription => '_SelectedGrabMouseCursor()';
}
diff --git a/packages/flutter/test/material/flat_button_test.dart b/packages/flutter/test/material/flat_button_test.dart
index 6e8f342..13c5f2e 100644
--- a/packages/flutter/test/material/flat_button_test.dart
+++ b/packages/flutter/test/material/flat_button_test.dart
@@ -437,6 +437,78 @@
await gesture.removePointer();
});
+ testWidgets('FlatButton changes mouse cursor when hovered', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ Directionality(
+ textDirection: TextDirection.ltr,
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: FlatButton.icon(
+ icon: const Icon(Icons.add),
+ label: const Text('Hello'),
+ onPressed: () {},
+ mouseCursor: SystemMouseCursors.text,
+ ),
+ ),
+ ),
+ );
+
+ final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
+ await gesture.addPointer(location: const Offset(1, 1));
+ addTearDown(gesture.removePointer);
+
+ await tester.pump();
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
+
+ await tester.pumpWidget(
+ Directionality(
+ textDirection: TextDirection.ltr,
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: FlatButton(
+ onPressed: () {},
+ mouseCursor: SystemMouseCursors.text,
+ child: const Text('Hello'),
+ ),
+ ),
+ ),
+ );
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
+
+ // Test default cursor
+ await tester.pumpWidget(
+ Directionality(
+ textDirection: TextDirection.ltr,
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: FlatButton(
+ onPressed: () {},
+ child: const Text('Hello'),
+ ),
+ ),
+ ),
+ );
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
+
+ // Test default cursor when disabled
+ await tester.pumpWidget(
+ const Directionality(
+ textDirection: TextDirection.ltr,
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: FlatButton(
+ onPressed: null,
+ child: Text('Hello'),
+ ),
+ ),
+ ),
+ );
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
+ });
+
testWidgets('Does FlatButton work with focus', (WidgetTester tester) async {
const Color focusColor = Color(0xff001122);
diff --git a/packages/flutter/test/material/floating_action_button_test.dart b/packages/flutter/test/material/floating_action_button_test.dart
index d2d6f22..9193935 100644
--- a/packages/flutter/test/material/floating_action_button_test.dart
+++ b/packages/flutter/test/material/floating_action_button_test.dart
@@ -744,6 +744,84 @@
);
});
+ testWidgets('Floating Action Button changes mouse cursor when hovered', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: Align(
+ alignment: Alignment.topLeft,
+ child: FloatingActionButton.extended(
+ onPressed: () { },
+ mouseCursor: SystemMouseCursors.text,
+ label: const Text('label'),
+ icon: const Icon(Icons.android),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
+ await gesture.addPointer(location: tester.getCenter(find.byType(FloatingActionButton)));
+ addTearDown(gesture.removePointer);
+
+ await tester.pump();
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
+
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: Align(
+ alignment: Alignment.topLeft,
+ child: FloatingActionButton(
+ onPressed: () { },
+ mouseCursor: SystemMouseCursors.text,
+ child: const Icon(Icons.add),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ await gesture.moveTo(tester.getCenter(find.byType(FloatingActionButton)));
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
+
+ // Test default cursor
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: Align(
+ alignment: Alignment.topLeft,
+ child: FloatingActionButton(
+ onPressed: () { },
+ child: const Icon(Icons.add),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
+
+ // Test default cursor when disabled
+ await tester.pumpWidget(
+ const MaterialApp(
+ home: Scaffold(
+ body: Align(
+ alignment: Alignment.topLeft,
+ child: FloatingActionButton(
+ onPressed: null,
+ child: Icon(Icons.add),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
+ });
+
testWidgets('Floating Action Button has no clip by default', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(
diff --git a/packages/flutter/test/material/icon_button_test.dart b/packages/flutter/test/material/icon_button_test.dart
index a699404..d582748 100644
--- a/packages/flutter/test/material/icon_button_test.dart
+++ b/packages/flutter/test/material/icon_button_test.dart
@@ -638,6 +638,49 @@
await tester.pumpAndSettle();
expect(box.size, equals(const Size(60, 40)));
});
+
+ testWidgets('IconButton.mouseCursor changes cursor on hover', (WidgetTester tester) async {
+ // Test argument works
+ await tester.pumpWidget(
+ Material(
+ child: Directionality(
+ textDirection: TextDirection.ltr,
+ child: Center(
+ child: IconButton(
+ onPressed: () {},
+ mouseCursor: SystemMouseCursors.forbidden,
+ icon: const Icon(Icons.play_arrow),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
+ await gesture.addPointer(location: tester.getCenter(find.byType(IconButton)));
+ addTearDown(gesture.removePointer);
+
+ await tester.pump();
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.forbidden);
+
+ // Test default is click
+ await tester.pumpWidget(
+ Material(
+ child: Directionality(
+ textDirection: TextDirection.ltr,
+ child: Center(
+ child: IconButton(
+ onPressed: () {},
+ icon: const Icon(Icons.play_arrow),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
+ });
}
Widget wrap({ Widget child }) {
diff --git a/packages/flutter/test/material/ink_well_test.dart b/packages/flutter/test/material/ink_well_test.dart
index 7ad5d9e..bd957c9 100644
--- a/packages/flutter/test/material/ink_well_test.dart
+++ b/packages/flutter/test/material/ink_well_test.dart
@@ -189,6 +189,64 @@
expect(inkFeatures, paintsExactlyCountTimes(#rect, 0));
});
+ testWidgets('InkWell.mouseCursor changes cursor on hover', (WidgetTester tester) async {
+ final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
+ await gesture.addPointer(location: const Offset(1, 1));
+ addTearDown(gesture.removePointer);
+
+ // Test argument works
+ await tester.pumpWidget(
+ Material(
+ child: Directionality(
+ textDirection: TextDirection.ltr,
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: InkWell(
+ mouseCursor: SystemMouseCursors.click,
+ onTap: () {},
+ ),
+ ),
+ ),
+ ),
+ );
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
+
+ // Test default of InkWell()
+ await tester.pumpWidget(
+ Material(
+ child: Directionality(
+ textDirection: TextDirection.ltr,
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: InkWell(
+ onTap: () {},
+ ),
+ ),
+ ),
+ ),
+ );
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.forbidden);
+
+ // Test default of InkResponse()
+ await tester.pumpWidget(
+ Material(
+ child: Directionality(
+ textDirection: TextDirection.ltr,
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: InkResponse(
+ onTap: () {},
+ ),
+ ),
+ ),
+ ),
+ );
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.forbidden);
+ });
+
group('feedback', () {
FeedbackTester feedback;
diff --git a/packages/flutter/test/material/list_tile_test.dart b/packages/flutter/test/material/list_tile_test.dart
index f91c98d..37ab63f 100644
--- a/packages/flutter/test/material/list_tile_test.dart
+++ b/packages/flutter/test/material/list_tile_test.dart
@@ -1443,4 +1443,67 @@
await tester.pumpAndSettle();
expect(box.size, equals(const Size(800, 44)));
});
+
+ testWidgets('ListTile changes mouse cursor when hovered', (WidgetTester tester) async {
+ // Test ListTile() constructor
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Material(
+ child: Center(
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: ListTile(
+ onTap: () {},
+ mouseCursor: SystemMouseCursors.text,
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
+ await gesture.addPointer(location: tester.getCenter(find.byType(ListTile)));
+ addTearDown(gesture.removePointer);
+
+ await tester.pump();
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
+
+ // Test default cursor
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Material(
+ child: Center(
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: ListTile(
+ onTap: () {},
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
+
+ // Test default cursor when disabled
+ await tester.pumpWidget(
+ const MaterialApp(
+ home: Material(
+ child: Center(
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: ListTile(
+ enabled: false,
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
+ });
}
diff --git a/packages/flutter/test/material/material_button_test.dart b/packages/flutter/test/material/material_button_test.dart
index 7bd183e..58d3cac 100644
--- a/packages/flutter/test/material/material_button_test.dart
+++ b/packages/flutter/test/material/material_button_test.dart
@@ -373,6 +373,59 @@
expect(didLongPressButton, isTrue);
});
+ testWidgets('MaterialButton changes mouse cursor when hovered', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ Directionality(
+ textDirection: TextDirection.ltr,
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: MaterialButton(
+ onPressed: () {},
+ mouseCursor: SystemMouseCursors.text,
+ ),
+ ),
+ ),
+ );
+
+ final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
+ await gesture.addPointer(location: Offset.zero);
+ addTearDown(gesture.removePointer);
+
+ await tester.pump();
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
+
+ // Test default cursor
+ await tester.pumpWidget(
+ Directionality(
+ textDirection: TextDirection.ltr,
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: MaterialButton(
+ onPressed: () {},
+ ),
+ ),
+ ),
+ );
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
+
+ // Test default cursor when disabled
+ await tester.pumpWidget(
+ const Directionality(
+ textDirection: TextDirection.ltr,
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: MaterialButton(
+ onPressed: null,
+ ),
+ ),
+ ),
+ );
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
+ });
+
// This test is very similar to the '...explicit splashColor and highlightColor' test
// in icon_button_test.dart. If you change this one, you may want to also change that one.
testWidgets('MaterialButton with explicit splashColor and highlightColor', (WidgetTester tester) async {
diff --git a/packages/flutter/test/material/outline_button_test.dart b/packages/flutter/test/material/outline_button_test.dart
index ea40ce2..8a69adb 100644
--- a/packages/flutter/test/material/outline_button_test.dart
+++ b/packages/flutter/test/material/outline_button_test.dart
@@ -109,6 +109,75 @@
gesture.removePointer();
});
+ testWidgets('OutlineButton changes mouse cursor when hovered', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ Directionality(
+ textDirection: TextDirection.ltr,
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: OutlineButton.icon(
+ icon: const Icon(Icons.add),
+ label: const Text('Hello'),
+ onPressed: () {},
+ mouseCursor: SystemMouseCursors.text,
+ ),
+ ),
+ ),
+ );
+
+ final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
+ await gesture.addPointer(location: const Offset(1, 1));
+ addTearDown(gesture.removePointer);
+
+ await tester.pump();
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
+
+ await tester.pumpWidget(
+ Directionality(
+ textDirection: TextDirection.ltr,
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: OutlineButton(
+ onPressed: () {},
+ mouseCursor: SystemMouseCursors.text,
+ ),
+ ),
+ ),
+ );
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
+
+ // Test default cursor
+ await tester.pumpWidget(
+ Directionality(
+ textDirection: TextDirection.ltr,
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: OutlineButton(
+ onPressed: () {},
+ ),
+ ),
+ ),
+ );
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
+
+ // Test default cursor when disabled
+ await tester.pumpWidget(
+ const Directionality(
+ textDirection: TextDirection.ltr,
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: OutlineButton(
+ onPressed: null,
+ ),
+ ),
+ ),
+ );
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
+ });
+
testWidgets('Does OutlineButton work with focus', (WidgetTester tester) async {
const Color focusColor = Color(0xff001122);
diff --git a/packages/flutter/test/material/popup_menu_test.dart b/packages/flutter/test/material/popup_menu_test.dart
index 58d1c1d..70b6139 100644
--- a/packages/flutter/test/material/popup_menu_test.dart
+++ b/packages/flutter/test/material/popup_menu_test.dart
@@ -5,6 +5,8 @@
import 'dart:ui' show window, SemanticsFlag;
import 'package:flutter_test/flutter_test.dart';
+import 'package:flutter/gestures.dart';
+import 'package:flutter/rendering.dart';
import 'package:flutter/material.dart';
import '../widgets/semantics_tester.dart';
@@ -1303,6 +1305,86 @@
expect(find.text('Tap me please!'), findsOneWidget);
});
+
+ testWidgets('PopupMenuItem changes mouse cursor when hovered', (WidgetTester tester) async {
+ const Key key = ValueKey<int>(1);
+ // Test PopupMenuItem() constructor
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: Align(
+ alignment: Alignment.topLeft,
+ child: Material(
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: PopupMenuItem<int>(
+ key: key,
+ mouseCursor: SystemMouseCursors.text,
+ value: 1,
+ child: Container(),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
+ await gesture.addPointer(location: tester.getCenter(find.byKey(key)));
+ addTearDown(gesture.removePointer);
+
+ await tester.pump();
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
+
+ // Test default cursor
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: Align(
+ alignment: Alignment.topLeft,
+ child: Material(
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: PopupMenuItem<int>(
+ key: key,
+ value: 1,
+ child: Container(),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
+
+ // Test default cursor when disabled
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: Align(
+ alignment: Alignment.topLeft,
+ child: Material(
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: PopupMenuItem<int>(
+ key: key,
+ value: 1,
+ enabled: false,
+ child: Container(),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
+ });
}
class TestApp extends StatefulWidget {
diff --git a/packages/flutter/test/material/radio_test.dart b/packages/flutter/test/material/radio_test.dart
index 3524dbd..ef7a378 100644
--- a/packages/flutter/test/material/radio_test.dart
+++ b/packages/flutter/test/material/radio_test.dart
@@ -613,4 +613,85 @@
await tester.pumpAndSettle();
expect(box.size, equals(const Size(60, 36)));
});
+
+ testWidgets('Radio changes mouse cursor when hovered', (WidgetTester tester) async {
+ const Key key = ValueKey<int>(1);
+ // Test Radio() constructor
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: Align(
+ alignment: Alignment.topLeft,
+ child: Material(
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: Radio<int>(
+ key: key,
+ mouseCursor: SystemMouseCursors.text,
+ value: 1,
+ onChanged: (int v) {},
+ groupValue: 2,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
+ await gesture.addPointer(location: tester.getCenter(find.byKey(key)));
+ addTearDown(gesture.removePointer);
+
+ await tester.pump();
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
+
+
+ // Test default cursor
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: Align(
+ alignment: Alignment.topLeft,
+ child: Material(
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: Radio<int>(
+ value: 1,
+ onChanged: (int v) {},
+ groupValue: 2,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
+
+ // Test default cursor when disabled
+ await tester.pumpWidget(
+ const MaterialApp(
+ home: Scaffold(
+ body: Align(
+ alignment: Alignment.topLeft,
+ child: Material(
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: Radio<int>(
+ value: 1,
+ onChanged: null,
+ groupValue: 2,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
+ });
}
diff --git a/packages/flutter/test/material/raised_button_test.dart b/packages/flutter/test/material/raised_button_test.dart
index cca0418..6bdd01c 100644
--- a/packages/flutter/test/material/raised_button_test.dart
+++ b/packages/flutter/test/material/raised_button_test.dart
@@ -431,6 +431,76 @@
await gesture.removePointer();
});
+ testWidgets('RaisedButton changes mouse cursor when hovered', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ Directionality(
+ textDirection: TextDirection.ltr,
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: RaisedButton.icon(
+ icon: const Icon(Icons.add),
+ label: const Text('Hello'),
+ onPressed: () {},
+ mouseCursor: SystemMouseCursors.text,
+ ),
+ ),
+ ),
+ );
+
+ final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
+ await gesture.addPointer(location: const Offset(1, 1));
+ addTearDown(gesture.removePointer);
+
+ await tester.pump();
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
+
+ await tester.pumpWidget(
+ Directionality(
+ textDirection: TextDirection.ltr,
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: RaisedButton(
+ onPressed: () {},
+ mouseCursor: SystemMouseCursors.text,
+ ),
+ ),
+ ),
+ );
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
+
+ // Test default cursor
+ await tester.pumpWidget(
+ Directionality(
+ textDirection: TextDirection.ltr,
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: RaisedButton(
+ onPressed: () {},
+ ),
+ ),
+ ),
+ );
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
+
+ // Test default cursor when disabled
+ await tester.pumpWidget(
+ const Directionality(
+ textDirection: TextDirection.ltr,
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: RaisedButton(
+ onPressed: null,
+ ),
+ ),
+ ),
+ );
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
+ });
+
+
testWidgets('Does RaisedButton work with focus', (WidgetTester tester) async {
const Color focusColor = Color(0xff001122);
diff --git a/packages/flutter/test/material/raw_material_button_test.dart b/packages/flutter/test/material/raw_material_button_test.dart
index 0efcc87..9047840 100644
--- a/packages/flutter/test/material/raw_material_button_test.dart
+++ b/packages/flutter/test/material/raw_material_button_test.dart
@@ -570,4 +570,57 @@
expect(box.size, equals(const Size(76, 36)));
expect(childRect, equals(const Rect.fromLTRB(372.0, 293.0, 428.0, 307.0)));
});
+
+ testWidgets('RawMaterialButton changes mouse cursor when hovered', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ Directionality(
+ textDirection: TextDirection.ltr,
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: RawMaterialButton(
+ onPressed: () {},
+ mouseCursor: SystemMouseCursors.text,
+ ),
+ ),
+ ),
+ );
+
+ final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
+ await gesture.addPointer(location: Offset.zero);
+ addTearDown(gesture.removePointer);
+
+ await tester.pump();
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
+
+ // Test default cursor
+ await tester.pumpWidget(
+ Directionality(
+ textDirection: TextDirection.ltr,
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: RawMaterialButton(
+ onPressed: () {},
+ ),
+ ),
+ ),
+ );
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
+
+ // Test default cursor when disabled
+ await tester.pumpWidget(
+ const Directionality(
+ textDirection: TextDirection.ltr,
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: RawMaterialButton(
+ onPressed: null,
+ ),
+ ),
+ ),
+ );
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
+ });
}
diff --git a/packages/flutter/test/material/slider_test.dart b/packages/flutter/test/material/slider_test.dart
index 5445347..6aa83ca 100644
--- a/packages/flutter/test/material/slider_test.dart
+++ b/packages/flutter/test/material/slider_test.dart
@@ -2136,6 +2136,82 @@
expect(renderObject.size.height, 200);
});
+ testWidgets('Slider changes mouse cursor when hovered', (WidgetTester tester) async {
+ // Test Slider() constructor
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Directionality(
+ textDirection: TextDirection.ltr,
+ child: Material(
+ child: Center(
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: Slider(
+ mouseCursor: SystemMouseCursors.text,
+ value: 0.5,
+ onChanged: (double newValue) { },
+ ),
+ ),
+ ),
+ ),
+ ),
+ )
+ );
+
+ final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
+ await gesture.addPointer(location: tester.getCenter(find.byType(Slider)));
+ addTearDown(gesture.removePointer);
+
+ await tester.pump();
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
+
+ // Test Slider.adaptive() constructor
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Directionality(
+ textDirection: TextDirection.ltr,
+ child: Material(
+ child: Center(
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: Slider.adaptive(
+ mouseCursor: SystemMouseCursors.text,
+ value: 0.5,
+ onChanged: (double newValue) { },
+ ),
+ ),
+ ),
+ ),
+ ),
+ )
+ );
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
+
+ // Test default cursor
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Directionality(
+ textDirection: TextDirection.ltr,
+ child: Material(
+ child: Center(
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: Slider(
+ value: 0.5,
+ onChanged: (double newValue) { },
+ ),
+ ),
+ ),
+ ),
+ ),
+ )
+ );
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
+ });
+
testWidgets('Slider implements debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
diff --git a/packages/flutter/test/material/switch_test.dart b/packages/flutter/test/material/switch_test.dart
index 6358654..3361e34 100644
--- a/packages/flutter/test/material/switch_test.dart
+++ b/packages/flutter/test/material/switch_test.dart
@@ -912,4 +912,105 @@
await tester.pumpAndSettle();
expect(value, isTrue);
});
+
+ testWidgets('Switch changes mouse cursor when hovered', (WidgetTester tester) async {
+ // Test Switch.adaptive() constructor
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: Align(
+ alignment: Alignment.topLeft,
+ child: Material(
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: Switch.adaptive(
+ mouseCursor: SystemMouseCursors.text,
+ value: true,
+ onChanged: (_) {},
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
+ await gesture.addPointer(location: tester.getCenter(find.byType(Switch)));
+ addTearDown(gesture.removePointer);
+
+ await tester.pump();
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
+
+ // Test Switch() constructor
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: Align(
+ alignment: Alignment.topLeft,
+ child: Material(
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: Switch(
+ mouseCursor: SystemMouseCursors.text,
+ value: true,
+ onChanged: (_) {},
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ await gesture.moveTo(tester.getCenter(find.byType(Switch)));
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
+
+ // Test default cursor
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: Align(
+ alignment: Alignment.topLeft,
+ child: Material(
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: Switch(
+ value: true,
+ onChanged: (_) {},
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
+
+ // Test default cursor when disabled
+ await tester.pumpWidget(
+ const MaterialApp(
+ home: Scaffold(
+ body: Align(
+ alignment: Alignment.topLeft,
+ child: Material(
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: Switch(
+ value: true,
+ onChanged: null,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
+
+ await tester.pumpAndSettle();
+ });
}
diff --git a/packages/flutter/test/material/tabs_test.dart b/packages/flutter/test/material/tabs_test.dart
index b4ec181..098366d 100644
--- a/packages/flutter/test/material/tabs_test.dart
+++ b/packages/flutter/test/material/tabs_test.dart
@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'package:flutter_test/flutter_test.dart';
+import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/rendering.dart';
@@ -2028,6 +2029,45 @@
expect(() => Tab(text: 'foo', child: Container()), throwsAssertionError);
});
+ testWidgets('Tabs changes mouse cursor when a tab is hovered', (WidgetTester tester) async {
+ final List<String> tabs = <String>['A', 'B'];
+ await tester.pumpWidget(MaterialApp(home: DefaultTabController(
+ length: tabs.length,
+ child: Scaffold(
+ body: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: TabBar(
+ mouseCursor: SystemMouseCursors.text,
+ tabs: tabs.map<Widget>((String tab) => Tab(text: tab)).toList(),
+ ),
+ ),
+ ),
+ ),
+ ));
+
+ final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
+ await gesture.addPointer(location: tester.getCenter(find.byType(Tab).first));
+ addTearDown(gesture.removePointer);
+
+ await tester.pump();
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
+
+ // Test default cursor
+ await tester.pumpWidget(MaterialApp(home: DefaultTabController(
+ length: tabs.length,
+ child: Scaffold(
+ body: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: TabBar(
+ tabs: tabs.map<Widget>((String tab) => Tab(text: tab)).toList(),
+ ),
+ ),
+ ),
+ ),
+ ));
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
+ });
testWidgets('TabController changes', (WidgetTester tester) async {
// This is a regression test for https://github.com/flutter/flutter/issues/14812
diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart
index 0e69b32..9e1b009 100644
--- a/packages/flutter/test/material/text_field_test.dart
+++ b/packages/flutter/test/material/text_field_test.dart
@@ -7788,4 +7788,31 @@
expect(triedToReadClipboard, true);
}
});
+
+ testWidgets('TextField changes mouse cursor when hovered', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ const MaterialApp(
+ home: Material(
+ child: TextField(
+ decoration: InputDecoration(
+ // Add an icon so that the left edge is not the text area
+ icon: Icon(Icons.person),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
+ await gesture.addPointer(location: tester.getCenter(find.byType(TextField)));
+ addTearDown(gesture.removePointer);
+
+ await tester.pump();
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
+
+ // Test top left, which is not the text area
+ await gesture.moveTo(tester.getTopLeft(find.byType(TextField)) + const Offset(1, 1));
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
+ });
}
diff --git a/packages/flutter/test/material/toggle_buttons_test.dart b/packages/flutter/test/material/toggle_buttons_test.dart
index 791df6d..992445b 100644
--- a/packages/flutter/test/material/toggle_buttons_test.dart
+++ b/packages/flutter/test/material/toggle_buttons_test.dart
@@ -4,6 +4,7 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
+import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart';
@@ -1434,4 +1435,74 @@
);
},
);
+
+ testWidgets('ToggleButtons changes mouse cursor when the button is hovered', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ Material(
+ child: boilerplate(
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: ToggleButtons(
+ mouseCursor: SystemMouseCursors.text,
+ onPressed: (int index) {},
+ isSelected: const <bool>[false, true],
+ children: const <Widget>[
+ Text('First child'),
+ Text('Second child'),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+
+ final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
+ await gesture.addPointer(location: tester.getCenter(find.text('First child')));
+ addTearDown(gesture.removePointer);
+
+ await tester.pump();
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
+
+ // Test default cursor
+ await tester.pumpWidget(
+ Material(
+ child: boilerplate(
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: ToggleButtons(
+ onPressed: (int index) {},
+ isSelected: const <bool>[false, true],
+ children: const <Widget>[
+ Text('First child'),
+ Text('Second child'),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
+
+ // Test default cursor when disabled
+ await tester.pumpWidget(
+ Material(
+ child: boilerplate(
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: ToggleButtons(
+ isSelected: const <bool>[false, true],
+ children: const <Widget>[
+ Text('First child'),
+ Text('Second child'),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
+ });
}
diff --git a/packages/flutter/test/widgets/actions_test.dart b/packages/flutter/test/widgets/actions_test.dart
index 82e1a98..78ef491 100644
--- a/packages/flutter/test/widgets/actions_test.dart
+++ b/packages/flutter/test/widgets/actions_test.dart
@@ -5,6 +5,7 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
+import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -317,6 +318,109 @@
expect(() => Actions.find<DoNothingIntent>(containerKey.currentContext), throwsAssertionError);
expect(Actions.find<DoNothingIntent>(containerKey.currentContext, nullOk: true), isNull);
});
+ testWidgets('FocusableActionDetector keeps track of focus and hover even when disabled.', (WidgetTester tester) async {
+ FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
+ final GlobalKey containerKey = GlobalKey();
+ bool invoked = false;
+ const Intent intent = TestIntent();
+ final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');
+ final Action<Intent> testAction = TestAction(
+ onInvoke: (Intent intent) {
+ invoked = true;
+ return invoked;
+ },
+ );
+ bool hovering = false;
+ bool focusing = false;
+
+ Future<void> buildTest(bool enabled) async {
+ await tester.pumpWidget(
+ Center(
+ child: Actions(
+ dispatcher: TestDispatcher1(postInvoke: collect),
+ actions: const <Type, Action<Intent>>{},
+ child: FocusableActionDetector(
+ enabled: enabled,
+ focusNode: focusNode,
+ shortcuts: <LogicalKeySet, Intent>{
+ LogicalKeySet(LogicalKeyboardKey.enter): intent,
+ },
+ actions: <Type, Action<Intent>>{
+ TestIntent: testAction,
+ },
+ onShowHoverHighlight: (bool value) => hovering = value,
+ onShowFocusHighlight: (bool value) => focusing = value,
+ child: Container(width: 100, height: 100, key: containerKey),
+ ),
+ ),
+ ),
+ );
+ return tester.pump();
+ }
+
+ await buildTest(true);
+ focusNode.requestFocus();
+ await tester.pump();
+ final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
+ addTearDown(gesture.removePointer);
+ await gesture.moveTo(tester.getCenter(find.byKey(containerKey)));
+ await tester.pump();
+ await tester.sendKeyEvent(LogicalKeyboardKey.enter);
+ expect(hovering, isTrue);
+ expect(focusing, isTrue);
+ expect(invoked, isTrue);
+
+ invoked = false;
+ await buildTest(false);
+ expect(hovering, isFalse);
+ expect(focusing, isFalse);
+ await tester.sendKeyEvent(LogicalKeyboardKey.enter);
+ await tester.pump();
+ expect(invoked, isFalse);
+ await buildTest(true);
+ expect(focusing, isFalse);
+ expect(hovering, isTrue);
+ await buildTest(false);
+ expect(focusing, isFalse);
+ expect(hovering, isFalse);
+ await gesture.moveTo(Offset.zero);
+ await buildTest(true);
+ expect(hovering, isFalse);
+ expect(focusing, isFalse);
+ });
+ testWidgets('FocusableActionDetector changes mouse cursor when hovered', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: FocusableActionDetector(
+ mouseCursor: SystemMouseCursors.text,
+ onShowHoverHighlight: (_) {},
+ onShowFocusHighlight: (_) {},
+ child: Container(),
+ ),
+ ),
+ );
+ final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
+ await gesture.addPointer(location: const Offset(1, 1));
+ addTearDown(gesture.removePointer);
+ await tester.pump();
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
+
+ // Test default
+ await tester.pumpWidget(
+ MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: FocusableActionDetector(
+ onShowHoverHighlight: (_) {},
+ onShowFocusHighlight: (_) {},
+ child: Container(),
+ ),
+ ),
+ );
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.forbidden);
+ });
});
group('Listening', () {
diff --git a/packages/flutter/test/widgets/editable_text_test.dart b/packages/flutter/test/widgets/editable_text_test.dart
index 1093db8..05c4c8e 100644
--- a/packages/flutter/test/widgets/editable_text_test.dart
+++ b/packages/flutter/test/widgets/editable_text_test.dart
@@ -6,6 +6,7 @@
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
+import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
@@ -4702,6 +4703,64 @@
state.updateEditingValue(const TextEditingValue(text: '\u{200E}🇧🇼🇧🇷🇮🇴 🇻🇬🇧🇳wahhh!🇧🇬🇧🇫 🇧🇮🇰ðŸ‡Ø¹ÙŽ عَ 🇨🇲 🇨🇦🇮🇨 🇨🇻🇧🇶 🇰🇾🇨🇫 🇹🇩🇨🇱 🇨🇳🇨🇽\u{200F}'));
expect(state.currentTextEditingValue.text, equals('\u{200E}🇧🇼🇧🇷🇮🇴 🇻🇬🇧🇳wahhh!🇧🇬🇧🇫 🇧🇮🇰ðŸ‡Ø¹ÙŽ عَ \u{200F}🇨🇲 🇨🇦🇮🇨 🇨🇻🇧🇶 🇰🇾🇨🇫 🇹🇩🇨🇱 🇨🇳🇨🇽\u{200F}'));
});
+
+ testWidgets('EditableText changes mouse cursor when hovered', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ MediaQuery(
+ data: const MediaQueryData(devicePixelRatio: 1.0),
+ child: Directionality(
+ textDirection: TextDirection.ltr,
+ child: FocusScope(
+ node: focusScopeNode,
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: EditableText(
+ controller: controller,
+ backgroundCursorColor: Colors.grey,
+ focusNode: focusNode,
+ style: textStyle,
+ cursorColor: cursorColor,
+ mouseCursor: SystemMouseCursors.click,
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
+ await gesture.addPointer(location: tester.getCenter(find.byType(EditableText)));
+ addTearDown(gesture.removePointer);
+
+ await tester.pump();
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
+
+ // Test default cursor
+ await tester.pumpWidget(
+ MediaQuery(
+ data: const MediaQueryData(devicePixelRatio: 1.0),
+ child: Directionality(
+ textDirection: TextDirection.ltr,
+ child: FocusScope(
+ node: focusScopeNode,
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: EditableText(
+ controller: controller,
+ backgroundCursorColor: Colors.grey,
+ focusNode: focusNode,
+ style: textStyle,
+ cursorColor: cursorColor,
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
+ });
}
class MockTextFormatter extends TextInputFormatter {
diff --git a/packages/flutter/test/widgets/modal_barrier_test.dart b/packages/flutter/test/widgets/modal_barrier_test.dart
index c501933..7a020a0 100644
--- a/packages/flutter/test/widgets/modal_barrier_test.dart
+++ b/packages/flutter/test/widgets/modal_barrier_test.dart
@@ -373,6 +373,24 @@
semantics.dispose();
});
+
+ testWidgets('ModalBarrier uses default mouse cursor', (WidgetTester tester) async {
+ await tester.pumpWidget(Stack(
+ textDirection: TextDirection.ltr,
+ children: const <Widget>[
+ MouseRegion(cursor: SystemMouseCursors.click),
+ ModalBarrier(dismissible: false),
+ ],
+ ));
+
+ final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
+ await gesture.addPointer(location: tester.getCenter(find.byType(ModalBarrier)));
+ addTearDown(gesture.removePointer);
+
+ await tester.pump();
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
+ });
}
class FirstWidget extends StatelessWidget {
diff --git a/packages/flutter/test/widgets/selectable_text_test.dart b/packages/flutter/test/widgets/selectable_text_test.dart
index 979749a..2df512d 100644
--- a/packages/flutter/test/widgets/selectable_text_test.dart
+++ b/packages/flutter/test/widgets/selectable_text_test.dart
@@ -3844,4 +3844,24 @@
// Long press triggers gesture recognizer.
expect(spyLongPress, 1);
});
+
+ testWidgets('SelectableText changes mouse cursor when hovered', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ const MaterialApp(
+ home: Material(
+ child: Center(
+ child: SelectableText('test'),
+ ),
+ ),
+ ),
+ );
+
+ final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
+ await gesture.addPointer(location: tester.getCenter(find.text('test')));
+ addTearDown(gesture.removePointer);
+
+ await tester.pump();
+
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
+ });
}