Unverified Commit 60f1aa25 authored by Tong Mu's avatar Tong Mu Committed by GitHub

Add mouse cursor API to widgets (phase 1) (#57628)

* Adds default cursor and/or mouseCursor property to a number of widgets.
* Adds `MaterialStateMouseCurrsor`.
parent be5b5c89
...@@ -6,6 +6,7 @@ import 'dart:collection' show Queue; ...@@ -6,6 +6,7 @@ import 'dart:collection' show Queue;
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter/rendering.dart';
import 'package:vector_math/vector_math_64.dart' show Vector3; import 'package:vector_math/vector_math_64.dart' show Vector3;
import 'bottom_navigation_bar_theme.dart'; import 'bottom_navigation_bar_theme.dart';
...@@ -187,6 +188,7 @@ class BottomNavigationBar extends StatefulWidget { ...@@ -187,6 +188,7 @@ class BottomNavigationBar extends StatefulWidget {
this.unselectedLabelStyle, this.unselectedLabelStyle,
this.showSelectedLabels = true, this.showSelectedLabels = true,
this.showUnselectedLabels, this.showUnselectedLabels,
this.mouseCursor,
}) : assert(items != null), }) : assert(items != null),
assert(items.length >= 2), assert(items.length >= 2),
assert( assert(
...@@ -314,6 +316,12 @@ class BottomNavigationBar extends StatefulWidget { ...@@ -314,6 +316,12 @@ class BottomNavigationBar extends StatefulWidget {
/// Whether the labels are shown for the unselected [BottomNavigationBarItem]s. /// Whether the labels are shown for the unselected [BottomNavigationBarItem]s.
final bool showSelectedLabels; 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 @override
_BottomNavigationBarState createState() => _BottomNavigationBarState(); _BottomNavigationBarState createState() => _BottomNavigationBarState();
} }
...@@ -337,12 +345,14 @@ class _BottomNavigationTile extends StatelessWidget { ...@@ -337,12 +345,14 @@ class _BottomNavigationTile extends StatelessWidget {
this.showSelectedLabels, this.showSelectedLabels,
this.showUnselectedLabels, this.showUnselectedLabels,
this.indexLabel, this.indexLabel,
@required this.mouseCursor,
}) : assert(type != null), }) : assert(type != null),
assert(item != null), assert(item != null),
assert(animation != null), assert(animation != null),
assert(selected != null), assert(selected != null),
assert(selectedLabelStyle != null), assert(selectedLabelStyle != null),
assert(unselectedLabelStyle != null); assert(unselectedLabelStyle != null),
assert(mouseCursor != null);
final BottomNavigationBarType type; final BottomNavigationBarType type;
final BottomNavigationBarItem item; final BottomNavigationBarItem item;
...@@ -359,6 +369,7 @@ class _BottomNavigationTile extends StatelessWidget { ...@@ -359,6 +369,7 @@ class _BottomNavigationTile extends StatelessWidget {
final String indexLabel; final String indexLabel;
final bool showSelectedLabels; final bool showSelectedLabels;
final bool showUnselectedLabels; final bool showUnselectedLabels;
final MouseCursor mouseCursor;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
...@@ -452,6 +463,7 @@ class _BottomNavigationTile extends StatelessWidget { ...@@ -452,6 +463,7 @@ class _BottomNavigationTile extends StatelessWidget {
children: <Widget>[ children: <Widget>[
InkResponse( InkResponse(
onTap: onTap, onTap: onTap,
mouseCursor: mouseCursor,
child: Padding( child: Padding(
padding: EdgeInsets.only(top: topPadding, bottom: bottomPadding), padding: EdgeInsets.only(top: topPadding, bottom: bottomPadding),
child: Column( child: Column(
...@@ -833,6 +845,7 @@ class _BottomNavigationBarState extends State<BottomNavigationBar> with TickerPr ...@@ -833,6 +845,7 @@ class _BottomNavigationBarState extends State<BottomNavigationBar> with TickerPr
); );
break; break;
} }
final MouseCursor effectiveMouseCursor = widget.mouseCursor ?? SystemMouseCursors.click;
final List<Widget> tiles = <Widget>[]; final List<Widget> tiles = <Widget>[];
for (int i = 0; i < widget.items.length; i++) { for (int i = 0; i < widget.items.length; i++) {
...@@ -855,6 +868,7 @@ class _BottomNavigationBarState extends State<BottomNavigationBar> with TickerPr ...@@ -855,6 +868,7 @@ class _BottomNavigationBarState extends State<BottomNavigationBar> with TickerPr
showSelectedLabels: widget.showSelectedLabels ?? bottomTheme.showSelectedLabels, showSelectedLabels: widget.showSelectedLabels ?? bottomTheme.showSelectedLabels,
showUnselectedLabels: widget.showUnselectedLabels ?? bottomTheme.showUnselectedLabels ?? _defaultShowUnselected, showUnselectedLabels: widget.showUnselectedLabels ?? bottomTheme.showUnselectedLabels ?? _defaultShowUnselected,
indexLabel: localizations.tabLabel(tabIndex: i + 1, tabCount: widget.items.length), indexLabel: localizations.tabLabel(tabIndex: i + 1, tabCount: widget.items.length),
mouseCursor: effectiveMouseCursor,
)); ));
} }
return tiles; return tiles;
......
...@@ -103,9 +103,20 @@ class RawMaterialButton extends StatefulWidget { ...@@ -103,9 +103,20 @@ class RawMaterialButton extends StatefulWidget {
/// [State.setState] is not allowed). /// [State.setState] is not allowed).
final ValueChanged<bool> onHighlightChanged; final ValueChanged<bool> onHighlightChanged;
/// {@macro flutter.material.inkwell.mousecursor} /// {@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; final MouseCursor mouseCursor;
/// Defines the default text style, with [Material.textStyle], for the /// Defines the default text style, with [Material.textStyle], for the
...@@ -373,6 +384,10 @@ class _RawMaterialButtonState extends State<RawMaterialButton> { ...@@ -373,6 +384,10 @@ class _RawMaterialButtonState extends State<RawMaterialButton> {
final ShapeBorder effectiveShape = MaterialStateProperty.resolveAs<ShapeBorder>(widget.shape, _states); final ShapeBorder effectiveShape = MaterialStateProperty.resolveAs<ShapeBorder>(widget.shape, _states);
final Offset densityAdjustment = widget.visualDensity.baseSizeAdjustment; final Offset densityAdjustment = widget.visualDensity.baseSizeAdjustment;
final BoxConstraints effectiveConstraints = widget.visualDensity.effectiveConstraints(widget.constraints); 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( final EdgeInsetsGeometry padding = widget.padding.add(
EdgeInsets.only( EdgeInsets.only(
left: densityAdjustment.dx, left: densityAdjustment.dx,
...@@ -382,6 +397,7 @@ class _RawMaterialButtonState extends State<RawMaterialButton> { ...@@ -382,6 +397,7 @@ class _RawMaterialButtonState extends State<RawMaterialButton> {
), ),
).clamp(EdgeInsets.zero, EdgeInsetsGeometry.infinity); ).clamp(EdgeInsets.zero, EdgeInsetsGeometry.infinity);
final Widget result = ConstrainedBox( final Widget result = ConstrainedBox(
constraints: effectiveConstraints, constraints: effectiveConstraints,
child: Material( child: Material(
...@@ -407,7 +423,7 @@ class _RawMaterialButtonState extends State<RawMaterialButton> { ...@@ -407,7 +423,7 @@ class _RawMaterialButtonState extends State<RawMaterialButton> {
onLongPress: widget.onLongPress, onLongPress: widget.onLongPress,
enableFeedback: widget.enableFeedback, enableFeedback: widget.enableFeedback,
customBorder: effectiveShape, customBorder: effectiveShape,
mouseCursor: widget.mouseCursor, mouseCursor: effectiveMouseCursor,
child: IconTheme.merge( child: IconTheme.merge(
data: IconThemeData(color: effectiveTextColor), data: IconThemeData(color: effectiveTextColor),
child: Container( child: Container(
......
...@@ -11,6 +11,7 @@ import 'package:flutter/widgets.dart'; ...@@ -11,6 +11,7 @@ import 'package:flutter/widgets.dart';
import 'constants.dart'; import 'constants.dart';
import 'debug.dart'; import 'debug.dart';
import 'material_state.dart';
import 'theme.dart'; import 'theme.dart';
import 'theme_data.dart'; import 'theme_data.dart';
import 'toggleable.dart'; import 'toggleable.dart';
...@@ -60,6 +61,7 @@ class Checkbox extends StatefulWidget { ...@@ -60,6 +61,7 @@ class Checkbox extends StatefulWidget {
@required this.value, @required this.value,
this.tristate = false, this.tristate = false,
@required this.onChanged, @required this.onChanged,
this.mouseCursor,
this.activeColor, this.activeColor,
this.checkColor, this.checkColor,
this.focusColor, this.focusColor,
...@@ -107,6 +109,23 @@ class Checkbox extends StatefulWidget { ...@@ -107,6 +109,23 @@ class Checkbox extends StatefulWidget {
/// ``` /// ```
final ValueChanged<bool> onChanged; 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. /// The color to use when this checkbox is checked.
/// ///
/// Defaults to [ThemeData.toggleableActiveColor]. /// Defaults to [ThemeData.toggleableActiveColor].
...@@ -226,6 +245,16 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin { ...@@ -226,6 +245,16 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin {
} }
size += (widget.visualDensity ?? themeData.visualDensity).baseSizeAdjustment; size += (widget.visualDensity ?? themeData.visualDensity).baseSizeAdjustment;
final BoxConstraints additionalConstraints = BoxConstraints.tight(size); 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( return FocusableActionDetector(
actions: _actionMap, actions: _actionMap,
focusNode: widget.focusNode, focusNode: widget.focusNode,
...@@ -233,6 +262,7 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin { ...@@ -233,6 +262,7 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin {
enabled: enabled, enabled: enabled,
onShowFocusHighlight: _handleFocusHighlightChanged, onShowFocusHighlight: _handleFocusHighlightChanged,
onShowHoverHighlight: _handleHoverChanged, onShowHoverHighlight: _handleHoverChanged,
mouseCursor: effectiveMouseCursor,
child: Builder( child: Builder(
builder: (BuildContext context) { builder: (BuildContext context) {
return _CheckboxRenderObjectWidget( return _CheckboxRenderObjectWidget(
...@@ -309,8 +339,10 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -309,8 +339,10 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
@override @override
void updateRenderObject(BuildContext context, _RenderCheckbox renderObject) { void updateRenderObject(BuildContext context, _RenderCheckbox renderObject) {
renderObject renderObject
..value = value // The `tristate` must be changed before `value` due to the assertion at
// the beginning of `set value`.
..tristate = tristate ..tristate = tristate
..value = value
..activeColor = activeColor ..activeColor = activeColor
..checkColor = checkColor ..checkColor = checkColor
..inactiveColor = inactiveColor ..inactiveColor = inactiveColor
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'button.dart'; import 'button.dart';
...@@ -104,6 +105,7 @@ class FlatButton extends MaterialButton { ...@@ -104,6 +105,7 @@ class FlatButton extends MaterialButton {
@required VoidCallback onPressed, @required VoidCallback onPressed,
VoidCallback onLongPress, VoidCallback onLongPress,
ValueChanged<bool> onHighlightChanged, ValueChanged<bool> onHighlightChanged,
MouseCursor mouseCursor,
ButtonTextTheme textTheme, ButtonTextTheme textTheme,
Color textColor, Color textColor,
Color disabledTextColor, Color disabledTextColor,
...@@ -129,6 +131,7 @@ class FlatButton extends MaterialButton { ...@@ -129,6 +131,7 @@ class FlatButton extends MaterialButton {
onPressed: onPressed, onPressed: onPressed,
onLongPress: onLongPress, onLongPress: onLongPress,
onHighlightChanged: onHighlightChanged, onHighlightChanged: onHighlightChanged,
mouseCursor: mouseCursor,
textTheme: textTheme, textTheme: textTheme,
textColor: textColor, textColor: textColor,
disabledTextColor: disabledTextColor, disabledTextColor: disabledTextColor,
...@@ -161,6 +164,7 @@ class FlatButton extends MaterialButton { ...@@ -161,6 +164,7 @@ class FlatButton extends MaterialButton {
@required VoidCallback onPressed, @required VoidCallback onPressed,
VoidCallback onLongPress, VoidCallback onLongPress,
ValueChanged<bool> onHighlightChanged, ValueChanged<bool> onHighlightChanged,
MouseCursor mouseCursor,
ButtonTextTheme textTheme, ButtonTextTheme textTheme,
Color textColor, Color textColor,
Color disabledTextColor, Color disabledTextColor,
...@@ -189,6 +193,7 @@ class FlatButton extends MaterialButton { ...@@ -189,6 +193,7 @@ class FlatButton extends MaterialButton {
onPressed: onPressed, onPressed: onPressed,
onLongPress: onLongPress, onLongPress: onLongPress,
onHighlightChanged: onHighlightChanged, onHighlightChanged: onHighlightChanged,
mouseCursor: mouseCursor,
fillColor: buttonTheme.getFillColor(this), fillColor: buttonTheme.getFillColor(this),
textStyle: theme.textTheme.button.copyWith(color: buttonTheme.getTextColor(this)), textStyle: theme.textTheme.button.copyWith(color: buttonTheme.getTextColor(this)),
focusColor: buttonTheme.getFocusColor(this), focusColor: buttonTheme.getFocusColor(this),
...@@ -224,6 +229,7 @@ class _FlatButtonWithIcon extends FlatButton with MaterialButtonWithIconMixin { ...@@ -224,6 +229,7 @@ class _FlatButtonWithIcon extends FlatButton with MaterialButtonWithIconMixin {
@required VoidCallback onPressed, @required VoidCallback onPressed,
VoidCallback onLongPress, VoidCallback onLongPress,
ValueChanged<bool> onHighlightChanged, ValueChanged<bool> onHighlightChanged,
MouseCursor mouseCursor,
ButtonTextTheme textTheme, ButtonTextTheme textTheme,
Color textColor, Color textColor,
Color disabledTextColor, Color disabledTextColor,
...@@ -251,6 +257,7 @@ class _FlatButtonWithIcon extends FlatButton with MaterialButtonWithIconMixin { ...@@ -251,6 +257,7 @@ class _FlatButtonWithIcon extends FlatButton with MaterialButtonWithIconMixin {
onPressed: onPressed, onPressed: onPressed,
onLongPress: onLongPress, onLongPress: onLongPress,
onHighlightChanged: onHighlightChanged, onHighlightChanged: onHighlightChanged,
mouseCursor: mouseCursor,
textTheme: textTheme, textTheme: textTheme,
textColor: textColor, textColor: textColor,
disabledTextColor: disabledTextColor, disabledTextColor: disabledTextColor,
......
...@@ -142,9 +142,9 @@ class FloatingActionButton extends StatelessWidget { ...@@ -142,9 +142,9 @@ class FloatingActionButton extends StatelessWidget {
this.highlightElevation, this.highlightElevation,
this.disabledElevation, this.disabledElevation,
@required this.onPressed, @required this.onPressed,
this.mouseCursor,
this.mini = false, this.mini = false,
this.shape, this.shape,
this.mouseCursor,
this.clipBehavior = Clip.none, this.clipBehavior = Clip.none,
this.focusNode, this.focusNode,
this.autofocus = false, this.autofocus = false,
...@@ -183,8 +183,8 @@ class FloatingActionButton extends StatelessWidget { ...@@ -183,8 +183,8 @@ class FloatingActionButton extends StatelessWidget {
this.highlightElevation, this.highlightElevation,
this.disabledElevation, this.disabledElevation,
@required this.onPressed, @required this.onPressed,
this.mouseCursor = SystemMouseCursors.click,
this.shape, this.shape,
this.mouseCursor,
this.isExtended = true, this.isExtended = true,
this.materialTapTargetSize, this.materialTapTargetSize,
this.clipBehavior = Clip.none, this.clipBehavior = Clip.none,
...@@ -290,6 +290,9 @@ class FloatingActionButton extends StatelessWidget { ...@@ -290,6 +290,9 @@ class FloatingActionButton extends StatelessWidget {
/// If this is set to null, the button will be disabled. /// If this is set to null, the button will be disabled.
final VoidCallback onPressed; final VoidCallback onPressed;
/// {@macro flutter.material.button.mouseCursor}
final MouseCursor mouseCursor;
/// The z-coordinate at which to place this button relative to its parent. /// 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. /// This controls the size of the shadow below the floating action button.
...@@ -378,11 +381,6 @@ class FloatingActionButton extends StatelessWidget { ...@@ -378,11 +381,6 @@ class FloatingActionButton extends StatelessWidget {
/// shape as well. /// shape as well.
final ShapeBorder shape; final ShapeBorder shape;
/// {@macro flutter.material.inkwell.mousecursor}
///
/// If the property is null, [SystemMouseCursor.click] is used.
final MouseCursor mouseCursor;
/// {@macro flutter.widgets.Clip} /// {@macro flutter.widgets.Clip}
/// ///
/// Defaults to [Clip.none], and must not be null. /// Defaults to [Clip.none], and must not be null.
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'constants.dart'; import 'constants.dart';
...@@ -151,6 +152,7 @@ class IconButton extends StatelessWidget { ...@@ -151,6 +152,7 @@ class IconButton extends StatelessWidget {
this.splashColor, this.splashColor,
this.disabledColor, this.disabledColor,
@required this.onPressed, @required this.onPressed,
this.mouseCursor = SystemMouseCursors.click,
this.focusNode, this.focusNode,
this.autofocus = false, this.autofocus = false,
this.tooltip, this.tooltip,
...@@ -274,6 +276,11 @@ class IconButton extends StatelessWidget { ...@@ -274,6 +276,11 @@ class IconButton extends StatelessWidget {
/// If this is set to null, the button will be disabled. /// If this is set to null, the button will be disabled.
final VoidCallback onPressed; final VoidCallback onPressed;
/// {@macro flutter.material.inkwell.mousecursor}
///
/// Defaults to [SystemMouseCursors.click].
final MouseCursor mouseCursor;
/// {@macro flutter.widgets.Focus.focusNode} /// {@macro flutter.widgets.Focus.focusNode}
final FocusNode focusNode; final FocusNode focusNode;
...@@ -370,6 +377,7 @@ class IconButton extends StatelessWidget { ...@@ -370,6 +377,7 @@ class IconButton extends StatelessWidget {
autofocus: autofocus, autofocus: autofocus,
canRequestFocus: onPressed != null, canRequestFocus: onPressed != null,
onTap: onPressed, onTap: onPressed,
mouseCursor: mouseCursor,
enableFeedback: enableFeedback, enableFeedback: enableFeedback,
child: result, child: result,
focusColor: focusColor ?? theme.focusColor, focusColor: focusColor ?? theme.focusColor,
......
...@@ -284,8 +284,8 @@ class InkResponse extends StatelessWidget { ...@@ -284,8 +284,8 @@ class InkResponse extends StatelessWidget {
/// ///
/// Must have an ancestor [Material] widget in which to cause ink reactions. /// Must have an ancestor [Material] widget in which to cause ink reactions.
/// ///
/// The [containedInkWell], [highlightShape], [enableFeedback], and /// The [mouseCursor], [containedInkWell], [highlightShape], [enableFeedback],
/// [excludeFromSemantics] arguments must not be null. /// and [excludeFromSemantics] arguments must not be null.
const InkResponse({ const InkResponse({
Key key, Key key,
this.child, this.child,
...@@ -296,7 +296,7 @@ class InkResponse extends StatelessWidget { ...@@ -296,7 +296,7 @@ class InkResponse extends StatelessWidget {
this.onLongPress, this.onLongPress,
this.onHighlightChanged, this.onHighlightChanged,
this.onHover, this.onHover,
this.mouseCursor, this.mouseCursor = MouseCursor.defer,
this.containedInkWell = false, this.containedInkWell = false,
this.highlightShape = BoxShape.circle, this.highlightShape = BoxShape.circle,
this.radius, this.radius,
...@@ -313,7 +313,8 @@ class InkResponse extends StatelessWidget { ...@@ -313,7 +313,8 @@ class InkResponse extends StatelessWidget {
this.canRequestFocus = true, this.canRequestFocus = true,
this.onFocusChange, this.onFocusChange,
this.autofocus = false, this.autofocus = false,
}) : assert(containedInkWell != null), }) : assert(mouseCursor != null),
assert(containedInkWell != null),
assert(highlightShape != null), assert(highlightShape != null),
assert(enableFeedback != null), assert(enableFeedback != null),
assert(excludeFromSemantics != null), assert(excludeFromSemantics != null),
...@@ -363,12 +364,11 @@ class InkResponse extends StatelessWidget { ...@@ -363,12 +364,11 @@ class InkResponse extends StatelessWidget {
/// material. /// material.
final ValueChanged<bool> onHover; final ValueChanged<bool> onHover;
/// {@template flutter.material.inkwell.mousecursor}
/// The cursor for a mouse pointer when it enters or is hovering over the /// The cursor for a mouse pointer when it enters or is hovering over the
/// region. /// widget.
/// {@endtemplate}
/// ///
/// 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; final MouseCursor mouseCursor;
/// Whether this ink response should be clipped its bounds. /// Whether this ink response should be clipped its bounds.
...@@ -544,7 +544,7 @@ class InkResponse extends StatelessWidget { ...@@ -544,7 +544,7 @@ class InkResponse extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final _ParentInkResponseState parentState = _ParentInkResponseProvider.of(context); final _ParentInkResponseState parentState = _ParentInkResponseProvider.of(context);
return _InnerInkResponse( return _InkResponseStateWidget(
child: child, child: child,
onTap: onTap, onTap: onTap,
onTapDown: onTapDown, onTapDown: onTapDown,
...@@ -553,7 +553,7 @@ class InkResponse extends StatelessWidget { ...@@ -553,7 +553,7 @@ class InkResponse extends StatelessWidget {
onLongPress: onLongPress, onLongPress: onLongPress,
onHighlightChanged: onHighlightChanged, onHighlightChanged: onHighlightChanged,
onHover: onHover, onHover: onHover,
mouseCursor: mouseCursor ?? SystemMouseCursors.click, mouseCursor: mouseCursor,
containedInkWell: containedInkWell, containedInkWell: containedInkWell,
highlightShape: highlightShape, highlightShape: highlightShape,
radius: radius, radius: radius,
...@@ -591,8 +591,8 @@ class InkResponse extends StatelessWidget { ...@@ -591,8 +591,8 @@ class InkResponse extends StatelessWidget {
} }
} }
class _InnerInkResponse extends StatefulWidget { class _InkResponseStateWidget extends StatefulWidget {
const _InnerInkResponse({ const _InkResponseStateWidget({
this.child, this.child,
this.onTap, this.onTap,
this.onTapDown, this.onTapDown,
...@@ -601,7 +601,7 @@ class _InnerInkResponse extends StatefulWidget { ...@@ -601,7 +601,7 @@ class _InnerInkResponse extends StatefulWidget {
this.onLongPress, this.onLongPress,
this.onHighlightChanged, this.onHighlightChanged,
this.onHover, this.onHover,
this.mouseCursor, this.mouseCursor = MouseCursor.defer,
this.containedInkWell = false, this.containedInkWell = false,
this.highlightShape = BoxShape.circle, this.highlightShape = BoxShape.circle,
this.radius, this.radius,
...@@ -626,7 +626,8 @@ class _InnerInkResponse extends StatefulWidget { ...@@ -626,7 +626,8 @@ class _InnerInkResponse extends StatefulWidget {
assert(enableFeedback != null), assert(enableFeedback != null),
assert(excludeFromSemantics != null), assert(excludeFromSemantics != null),
assert(autofocus != null), assert(autofocus != null),
assert(canRequestFocus != null); assert(canRequestFocus != null),
assert(mouseCursor != null);
final Widget child; final Widget child;
final GestureTapCallback onTap; final GestureTapCallback onTap;
...@@ -671,6 +672,7 @@ class _InnerInkResponse extends StatefulWidget { ...@@ -671,6 +672,7 @@ class _InnerInkResponse extends StatefulWidget {
if (onTapCancel != null) 'tap cancel', if (onTapCancel != null) 'tap cancel',
]; ];
properties.add(IterableProperty<String>('gestures', gestures, ifEmpty: '<none>')); 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<bool>('containedInkWell', containedInkWell, level: DiagnosticLevel.fine));
properties.add(DiagnosticsProperty<BoxShape>( properties.add(DiagnosticsProperty<BoxShape>(
'highlightShape', 'highlightShape',
...@@ -689,8 +691,8 @@ enum _HighlightType { ...@@ -689,8 +691,8 @@ enum _HighlightType {
focus, focus,
} }
class _InkResponseState extends State<_InnerInkResponse> class _InkResponseState extends State<_InkResponseStateWidget>
with AutomaticKeepAliveClientMixin<_InnerInkResponse> with AutomaticKeepAliveClientMixin<_InkResponseStateWidget>
implements _ParentInkResponseState { implements _ParentInkResponseState {
Set<InteractiveInkFeature> _splashes; Set<InteractiveInkFeature> _splashes;
InteractiveInkFeature _currentSplash; InteractiveInkFeature _currentSplash;
...@@ -732,7 +734,7 @@ class _InkResponseState extends State<_InnerInkResponse> ...@@ -732,7 +734,7 @@ class _InkResponseState extends State<_InnerInkResponse>
} }
@override @override
void didUpdateWidget(_InnerInkResponse oldWidget) { void didUpdateWidget(_InkResponseStateWidget oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (_isWidgetEnabled(widget) != _isWidgetEnabled(oldWidget)) { if (_isWidgetEnabled(widget) != _isWidgetEnabled(oldWidget)) {
_handleHoverChange(_hovering); _handleHoverChange(_hovering);
...@@ -988,7 +990,7 @@ class _InkResponseState extends State<_InnerInkResponse> ...@@ -988,7 +990,7 @@ class _InkResponseState extends State<_InnerInkResponse>
super.deactivate(); super.deactivate();
} }
bool _isWidgetEnabled(_InnerInkResponse widget) { bool _isWidgetEnabled(_InkResponseStateWidget widget) {
return widget.onTap != null || widget.onDoubleTap != null || widget.onLongPress != null; return widget.onTap != null || widget.onDoubleTap != null || widget.onLongPress != null;
} }
...@@ -1146,8 +1148,8 @@ class InkWell extends InkResponse { ...@@ -1146,8 +1148,8 @@ class InkWell extends InkResponse {
/// ///
/// Must have an ancestor [Material] widget in which to cause ink reactions. /// Must have an ancestor [Material] widget in which to cause ink reactions.
/// ///
/// The [enableFeedback] and [excludeFromSemantics] arguments must not be /// The [mouseCursor], [enableFeedback], and [excludeFromSemantics] arguments
/// null. /// must not be null.
const InkWell({ const InkWell({
Key key, Key key,
Widget child, Widget child,
...@@ -1158,7 +1160,7 @@ class InkWell extends InkResponse { ...@@ -1158,7 +1160,7 @@ class InkWell extends InkResponse {
GestureTapCancelCallback onTapCancel, GestureTapCancelCallback onTapCancel,
ValueChanged<bool> onHighlightChanged, ValueChanged<bool> onHighlightChanged,
ValueChanged<bool> onHover, ValueChanged<bool> onHover,
MouseCursor mouseCursor, MouseCursor mouseCursor = MouseCursor.defer,
Color focusColor, Color focusColor,
Color hoverColor, Color hoverColor,
Color highlightColor, Color highlightColor,
......
...@@ -13,6 +13,7 @@ import 'constants.dart'; ...@@ -13,6 +13,7 @@ import 'constants.dart';
import 'debug.dart'; import 'debug.dart';
import 'divider.dart'; import 'divider.dart';
import 'ink_well.dart'; import 'ink_well.dart';
import 'material_state.dart';
import 'theme.dart'; import 'theme.dart';
import 'theme_data.dart'; import 'theme_data.dart';
...@@ -640,6 +641,7 @@ class ListTile extends StatelessWidget { ...@@ -640,6 +641,7 @@ class ListTile extends StatelessWidget {
this.enabled = true, this.enabled = true,
this.onTap, this.onTap,
this.onLongPress, this.onLongPress,
this.mouseCursor,
this.selected = false, this.selected = false,
this.focusColor, this.focusColor,
this.hoverColor, this.hoverColor,
...@@ -736,6 +738,18 @@ class ListTile extends StatelessWidget { ...@@ -736,6 +738,18 @@ class ListTile extends StatelessWidget {
/// Inoperative if [enabled] is false. /// Inoperative if [enabled] is false.
final GestureLongPressCallback onLongPress; 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. /// 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 /// By default the selected color is the theme's primary color. The selected color
...@@ -909,9 +923,18 @@ class ListTile extends StatelessWidget { ...@@ -909,9 +923,18 @@ class ListTile extends StatelessWidget {
?? tileTheme?.contentPadding?.resolve(textDirection) ?? tileTheme?.contentPadding?.resolve(textDirection)
?? _defaultContentPadding; ?? _defaultContentPadding;
final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor>(
mouseCursor ?? MaterialStateMouseCursor.clickable,
<MaterialState>{
if (!enabled) MaterialState.disabled,
if (selected) MaterialState.selected,
},
);
return InkWell( return InkWell(
onTap: enabled ? onTap : null, onTap: enabled ? onTap : null,
onLongPress: enabled ? onLongPress : null, onLongPress: enabled ? onLongPress : null,
mouseCursor: effectiveMouseCursor,
canRequestFocus: enabled, canRequestFocus: enabled,
focusNode: focusNode, focusNode: focusNode,
focusColor: focusColor, focusColor: focusColor,
......
...@@ -53,6 +53,7 @@ class MaterialButton extends StatelessWidget { ...@@ -53,6 +53,7 @@ class MaterialButton extends StatelessWidget {
@required this.onPressed, @required this.onPressed,
this.onLongPress, this.onLongPress,
this.onHighlightChanged, this.onHighlightChanged,
this.mouseCursor,
this.textTheme, this.textTheme,
this.textColor, this.textColor,
this.disabledTextColor, this.disabledTextColor,
...@@ -115,6 +116,9 @@ class MaterialButton extends StatelessWidget { ...@@ -115,6 +116,9 @@ class MaterialButton extends StatelessWidget {
/// [State.setState] is not allowed). /// [State.setState] is not allowed).
final ValueChanged<bool> onHighlightChanged; 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 /// Defines the button's base colors, and the defaults for the button's minimum
/// size, internal padding, and shape. /// size, internal padding, and shape.
/// ///
...@@ -387,6 +391,7 @@ class MaterialButton extends StatelessWidget { ...@@ -387,6 +391,7 @@ class MaterialButton extends StatelessWidget {
onLongPress: onLongPress, onLongPress: onLongPress,
enableFeedback: enableFeedback, enableFeedback: enableFeedback,
onHighlightChanged: onHighlightChanged, onHighlightChanged: onHighlightChanged,
mouseCursor: mouseCursor,
fillColor: buttonTheme.getFillColor(this), fillColor: buttonTheme.getFillColor(this),
textStyle: theme.textTheme.button.copyWith(color: buttonTheme.getTextColor(this)), textStyle: theme.textTheme.button.copyWith(color: buttonTheme.getTextColor(this)),
focusColor: focusColor ?? buttonTheme.getFocusColor(this) ?? theme.focusColor, focusColor: focusColor ?? buttonTheme.getFocusColor(this) ?? theme.focusColor,
......
...@@ -4,6 +4,9 @@ ...@@ -4,6 +4,9 @@
import 'dart:ui' show Color; 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 /// Interactive states that some of the Material widgets can take on when
/// receiving input from the user. /// receiving input from the user.
/// ///
...@@ -178,6 +181,98 @@ class _MaterialStateColor extends MaterialStateColor { ...@@ -178,6 +181,98 @@ class _MaterialStateColor extends MaterialStateColor {
Color resolve(Set<MaterialState> states) => _resolve(states); 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 /// Interface for classes that can return a value of type `T` based on a set of
/// [MaterialState]s. /// [MaterialState]s.
/// ///
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'button_theme.dart'; import 'button_theme.dart';
...@@ -63,6 +64,7 @@ class OutlineButton extends MaterialButton { ...@@ -63,6 +64,7 @@ class OutlineButton extends MaterialButton {
Key key, Key key,
@required VoidCallback onPressed, @required VoidCallback onPressed,
VoidCallback onLongPress, VoidCallback onLongPress,
MouseCursor mouseCursor,
ButtonTextTheme textTheme, ButtonTextTheme textTheme,
Color textColor, Color textColor,
Color disabledTextColor, Color disabledTextColor,
...@@ -89,6 +91,7 @@ class OutlineButton extends MaterialButton { ...@@ -89,6 +91,7 @@ class OutlineButton extends MaterialButton {
key: key, key: key,
onPressed: onPressed, onPressed: onPressed,
onLongPress: onLongPress, onLongPress: onLongPress,
mouseCursor: mouseCursor,
textTheme: textTheme, textTheme: textTheme,
textColor: textColor, textColor: textColor,
disabledTextColor: disabledTextColor, disabledTextColor: disabledTextColor,
...@@ -119,6 +122,7 @@ class OutlineButton extends MaterialButton { ...@@ -119,6 +122,7 @@ class OutlineButton extends MaterialButton {
Key key, Key key,
@required VoidCallback onPressed, @required VoidCallback onPressed,
VoidCallback onLongPress, VoidCallback onLongPress,
MouseCursor mouseCursor,
ButtonTextTheme textTheme, ButtonTextTheme textTheme,
Color textColor, Color textColor,
Color disabledTextColor, Color disabledTextColor,
...@@ -178,6 +182,7 @@ class OutlineButton extends MaterialButton { ...@@ -178,6 +182,7 @@ class OutlineButton extends MaterialButton {
autofocus: autofocus, autofocus: autofocus,
onPressed: onPressed, onPressed: onPressed,
onLongPress: onLongPress, onLongPress: onLongPress,
mouseCursor: mouseCursor,
brightness: buttonTheme.getBrightness(this), brightness: buttonTheme.getBrightness(this),
textTheme: textTheme, textTheme: textTheme,
textColor: buttonTheme.getTextColor(this), textColor: buttonTheme.getTextColor(this),
...@@ -218,6 +223,7 @@ class _OutlineButtonWithIcon extends OutlineButton with MaterialButtonWithIconMi ...@@ -218,6 +223,7 @@ class _OutlineButtonWithIcon extends OutlineButton with MaterialButtonWithIconMi
Key key, Key key,
@required VoidCallback onPressed, @required VoidCallback onPressed,
VoidCallback onLongPress, VoidCallback onLongPress,
MouseCursor mouseCursor,
ButtonTextTheme textTheme, ButtonTextTheme textTheme,
Color textColor, Color textColor,
Color disabledTextColor, Color disabledTextColor,
...@@ -247,6 +253,7 @@ class _OutlineButtonWithIcon extends OutlineButton with MaterialButtonWithIconMi ...@@ -247,6 +253,7 @@ class _OutlineButtonWithIcon extends OutlineButton with MaterialButtonWithIconMi
key: key, key: key,
onPressed: onPressed, onPressed: onPressed,
onLongPress: onLongPress, onLongPress: onLongPress,
mouseCursor: mouseCursor,
textTheme: textTheme, textTheme: textTheme,
textColor: textColor, textColor: textColor,
disabledTextColor: disabledTextColor, disabledTextColor: disabledTextColor,
...@@ -281,6 +288,7 @@ class _OutlineButton extends StatefulWidget { ...@@ -281,6 +288,7 @@ class _OutlineButton extends StatefulWidget {
Key key, Key key,
@required this.onPressed, @required this.onPressed,
this.onLongPress, this.onLongPress,
this.mouseCursor,
this.brightness, this.brightness,
this.textTheme, this.textTheme,
this.textColor, this.textColor,
...@@ -309,6 +317,7 @@ class _OutlineButton extends StatefulWidget { ...@@ -309,6 +317,7 @@ class _OutlineButton extends StatefulWidget {
final VoidCallback onPressed; final VoidCallback onPressed;
final VoidCallback onLongPress; final VoidCallback onLongPress;
final MouseCursor mouseCursor;
final Brightness brightness; final Brightness brightness;
final ButtonTextTheme textTheme; final ButtonTextTheme textTheme;
final Color textColor; final Color textColor;
...@@ -462,6 +471,7 @@ class _OutlineButtonState extends State<_OutlineButton> with SingleTickerProvide ...@@ -462,6 +471,7 @@ class _OutlineButtonState extends State<_OutlineButton> with SingleTickerProvide
disabledColor: Colors.transparent, disabledColor: Colors.transparent,
onPressed: widget.onPressed, onPressed: widget.onPressed,
onLongPress: widget.onLongPress, onLongPress: widget.onLongPress,
mouseCursor: widget.mouseCursor,
elevation: 0.0, elevation: 0.0,
disabledElevation: 0.0, disabledElevation: 0.0,
focusElevation: 0.0, focusElevation: 0.0,
......
...@@ -17,6 +17,7 @@ import 'ink_well.dart'; ...@@ -17,6 +17,7 @@ import 'ink_well.dart';
import 'list_tile.dart'; import 'list_tile.dart';
import 'material.dart'; import 'material.dart';
import 'material_localizations.dart'; import 'material_localizations.dart';
import 'material_state.dart';
import 'popup_menu_theme.dart'; import 'popup_menu_theme.dart';
import 'theme.dart'; import 'theme.dart';
import 'tooltip.dart'; import 'tooltip.dart';
...@@ -216,6 +217,7 @@ class PopupMenuItem<T> extends PopupMenuEntry<T> { ...@@ -216,6 +217,7 @@ class PopupMenuItem<T> extends PopupMenuEntry<T> {
this.enabled = true, this.enabled = true,
this.height = kMinInteractiveDimension, this.height = kMinInteractiveDimension,
this.textStyle, this.textStyle,
this.mouseCursor,
@required this.child, @required this.child,
}) : assert(enabled != null), }) : assert(enabled != null),
assert(height != null), assert(height != null),
...@@ -242,6 +244,17 @@ class PopupMenuItem<T> extends PopupMenuEntry<T> { ...@@ -242,6 +244,17 @@ class PopupMenuItem<T> extends PopupMenuEntry<T> {
/// If [PopupMenuThemeData.textStyle] is also null, then [ThemeData.textTheme.subtitle1] is used. /// If [PopupMenuThemeData.textStyle] is also null, then [ThemeData.textTheme.subtitle1] is used.
final TextStyle textStyle; 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. /// The widget below this widget in the tree.
/// ///
/// Typically a single-line [ListTile] (for menus with icons) or a [Text]. An /// Typically a single-line [ListTile] (for menus with icons) or a [Text]. An
...@@ -320,10 +333,17 @@ class PopupMenuItemState<T, W extends PopupMenuItem<T>> extends State<W> { ...@@ -320,10 +333,17 @@ class PopupMenuItemState<T, W extends PopupMenuItem<T>> extends State<W> {
child: item, child: item,
); );
} }
final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor>(
widget.mouseCursor ?? MaterialStateMouseCursor.clickable,
<MaterialState>{
if (!widget.enabled) MaterialState.disabled,
},
);
return InkWell( return InkWell(
onTap: widget.enabled ? handleTap : null, onTap: widget.enabled ? handleTap : null,
canRequestFocus: widget.enabled, canRequestFocus: widget.enabled,
mouseCursor: effectiveMouseCursor,
child: item, child: item,
); );
} }
......
...@@ -9,6 +9,7 @@ import 'package:flutter/widgets.dart'; ...@@ -9,6 +9,7 @@ import 'package:flutter/widgets.dart';
import 'constants.dart'; import 'constants.dart';
import 'debug.dart'; import 'debug.dart';
import 'material_state.dart';
import 'theme.dart'; import 'theme.dart';
import 'theme_data.dart'; import 'theme_data.dart';
import 'toggleable.dart'; import 'toggleable.dart';
...@@ -108,6 +109,7 @@ class Radio<T> extends StatefulWidget { ...@@ -108,6 +109,7 @@ class Radio<T> extends StatefulWidget {
@required this.value, @required this.value,
@required this.groupValue, @required this.groupValue,
@required this.onChanged, @required this.onChanged,
this.mouseCursor,
this.toggleable = false, this.toggleable = false,
this.activeColor, this.activeColor,
this.focusColor, this.focusColor,
...@@ -157,6 +159,20 @@ class Radio<T> extends StatefulWidget { ...@@ -157,6 +159,20 @@ class Radio<T> extends StatefulWidget {
/// ``` /// ```
final ValueChanged<T> onChanged; 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 /// Set to true if this radio button is allowed to be returned to an
/// indeterminate state by selecting it again when selected. /// indeterminate state by selecting it again when selected.
/// ///
...@@ -325,17 +341,29 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin { ...@@ -325,17 +341,29 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin {
} }
size += (widget.visualDensity ?? themeData.visualDensity).baseSizeAdjustment; size += (widget.visualDensity ?? themeData.visualDensity).baseSizeAdjustment;
final BoxConstraints additionalConstraints = BoxConstraints.tight(size); 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( return FocusableActionDetector(
actions: _actionMap, actions: _actionMap,
focusNode: widget.focusNode, focusNode: widget.focusNode,
autofocus: widget.autofocus, autofocus: widget.autofocus,
mouseCursor: effectiveMouseCursor,
enabled: enabled, enabled: enabled,
onShowFocusHighlight: _handleHighlightChanged, onShowFocusHighlight: _handleHighlightChanged,
onShowHoverHighlight: _handleHoverChanged, onShowHoverHighlight: _handleHoverChanged,
child: Builder( child: Builder(
builder: (BuildContext context) { builder: (BuildContext context) {
return _RadioRenderObjectWidget( return _RadioRenderObjectWidget(
selected: widget.value == widget.groupValue, selected: selected,
activeColor: widget.activeColor ?? themeData.toggleableActiveColor, activeColor: widget.activeColor ?? themeData.toggleableActiveColor,
inactiveColor: _getInactiveColor(themeData), inactiveColor: _getInactiveColor(themeData),
focusColor: widget.focusColor ?? themeData.focusColor, focusColor: widget.focusColor ?? themeData.focusColor,
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'button.dart'; import 'button.dart';
...@@ -110,6 +111,7 @@ class RaisedButton extends MaterialButton { ...@@ -110,6 +111,7 @@ class RaisedButton extends MaterialButton {
@required VoidCallback onPressed, @required VoidCallback onPressed,
VoidCallback onLongPress, VoidCallback onLongPress,
ValueChanged<bool> onHighlightChanged, ValueChanged<bool> onHighlightChanged,
MouseCursor mouseCursor,
ButtonTextTheme textTheme, ButtonTextTheme textTheme,
Color textColor, Color textColor,
Color disabledTextColor, Color disabledTextColor,
...@@ -146,6 +148,7 @@ class RaisedButton extends MaterialButton { ...@@ -146,6 +148,7 @@ class RaisedButton extends MaterialButton {
onPressed: onPressed, onPressed: onPressed,
onLongPress: onLongPress, onLongPress: onLongPress,
onHighlightChanged: onHighlightChanged, onHighlightChanged: onHighlightChanged,
mouseCursor: mouseCursor,
textTheme: textTheme, textTheme: textTheme,
textColor: textColor, textColor: textColor,
disabledTextColor: disabledTextColor, disabledTextColor: disabledTextColor,
...@@ -185,6 +188,7 @@ class RaisedButton extends MaterialButton { ...@@ -185,6 +188,7 @@ class RaisedButton extends MaterialButton {
@required VoidCallback onPressed, @required VoidCallback onPressed,
VoidCallback onLongPress, VoidCallback onLongPress,
ValueChanged<bool> onHighlightChanged, ValueChanged<bool> onHighlightChanged,
MouseCursor mouseCursor,
ButtonTextTheme textTheme, ButtonTextTheme textTheme,
Color textColor, Color textColor,
Color disabledTextColor, Color disabledTextColor,
...@@ -217,6 +221,7 @@ class RaisedButton extends MaterialButton { ...@@ -217,6 +221,7 @@ class RaisedButton extends MaterialButton {
onPressed: onPressed, onPressed: onPressed,
onLongPress: onLongPress, onLongPress: onLongPress,
onHighlightChanged: onHighlightChanged, onHighlightChanged: onHighlightChanged,
mouseCursor: mouseCursor,
clipBehavior: clipBehavior, clipBehavior: clipBehavior,
fillColor: buttonTheme.getFillColor(this), fillColor: buttonTheme.getFillColor(this),
textStyle: theme.textTheme.button.copyWith(color: buttonTheme.getTextColor(this)), textStyle: theme.textTheme.button.copyWith(color: buttonTheme.getTextColor(this)),
...@@ -262,6 +267,7 @@ class _RaisedButtonWithIcon extends RaisedButton with MaterialButtonWithIconMixi ...@@ -262,6 +267,7 @@ class _RaisedButtonWithIcon extends RaisedButton with MaterialButtonWithIconMixi
@required VoidCallback onPressed, @required VoidCallback onPressed,
VoidCallback onLongPress, VoidCallback onLongPress,
ValueChanged<bool> onHighlightChanged, ValueChanged<bool> onHighlightChanged,
MouseCursor mouseCursor,
ButtonTextTheme textTheme, ButtonTextTheme textTheme,
Color textColor, Color textColor,
Color disabledTextColor, Color disabledTextColor,
...@@ -296,6 +302,7 @@ class _RaisedButtonWithIcon extends RaisedButton with MaterialButtonWithIconMixi ...@@ -296,6 +302,7 @@ class _RaisedButtonWithIcon extends RaisedButton with MaterialButtonWithIconMixi
onPressed: onPressed, onPressed: onPressed,
onLongPress: onLongPress, onLongPress: onLongPress,
onHighlightChanged: onHighlightChanged, onHighlightChanged: onHighlightChanged,
mouseCursor: mouseCursor,
textTheme: textTheme, textTheme: textTheme,
textColor: textColor, textColor: textColor,
disabledTextColor: disabledTextColor, disabledTextColor: disabledTextColor,
......
...@@ -17,6 +17,7 @@ import 'package:flutter/widgets.dart'; ...@@ -17,6 +17,7 @@ import 'package:flutter/widgets.dart';
import 'constants.dart'; import 'constants.dart';
import 'debug.dart'; import 'debug.dart';
import 'material.dart'; import 'material.dart';
import 'material_state.dart';
import 'slider_theme.dart'; import 'slider_theme.dart';
import 'theme.dart'; import 'theme.dart';
...@@ -159,6 +160,7 @@ class Slider extends StatefulWidget { ...@@ -159,6 +160,7 @@ class Slider extends StatefulWidget {
this.label, this.label,
this.activeColor, this.activeColor,
this.inactiveColor, this.inactiveColor,
this.mouseCursor,
this.semanticFormatterCallback, this.semanticFormatterCallback,
this.focusNode, this.focusNode,
this.autofocus = false, this.autofocus = false,
...@@ -188,6 +190,7 @@ class Slider extends StatefulWidget { ...@@ -188,6 +190,7 @@ class Slider extends StatefulWidget {
this.max = 1.0, this.max = 1.0,
this.divisions, this.divisions,
this.label, this.label,
this.mouseCursor,
this.activeColor, this.activeColor,
this.inactiveColor, this.inactiveColor,
this.semanticFormatterCallback, this.semanticFormatterCallback,
...@@ -381,6 +384,19 @@ class Slider extends StatefulWidget { ...@@ -381,6 +384,19 @@ class Slider extends StatefulWidget {
/// Ignored if this slider is created with [Slider.adaptive]. /// Ignored if this slider is created with [Slider.adaptive].
final Color inactiveColor; 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. /// The callback used to create a semantic value from a slider value.
/// ///
/// Defaults to formatting values as a percentage. /// Defaults to formatting values as a percentage.
...@@ -537,7 +553,7 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin { ...@@ -537,7 +553,7 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
widget.onChangeEnd(_lerp(value)); widget.onChangeEnd(_lerp(value));
} }
void _actionHandler (_AdjustSliderIntent intent) { void _actionHandler(_AdjustSliderIntent intent) {
final _RenderSlider renderSlider = _renderObjectKey.currentContext.findRenderObject() as _RenderSlider; final _RenderSlider renderSlider = _renderObjectKey.currentContext.findRenderObject() as _RenderSlider;
final TextDirection textDirection = Directionality.of(_renderObjectKey.currentContext); final TextDirection textDirection = Directionality.of(_renderObjectKey.currentContext);
switch (intent.type) { switch (intent.type) {
...@@ -682,6 +698,14 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin { ...@@ -682,6 +698,14 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
color: theme.colorScheme.onPrimary, 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 // 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 // indicators It must be kept in sync with the function with the same name
...@@ -696,6 +720,7 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin { ...@@ -696,6 +720,7 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
enabled: _enabled, enabled: _enabled,
onShowFocusHighlight: _handleFocusHighlightChanged, onShowFocusHighlight: _handleFocusHighlightChanged,
onShowHoverHighlight: _handleHoverChanged, onShowHoverHighlight: _handleHoverChanged,
mouseCursor: effectiveMouseCursor,
child: CompositedTransformTarget( child: CompositedTransformTarget(
link: _layerLink, link: _layerLink,
child: _SliderRenderObjectWidget( child: _SliderRenderObjectWidget(
......
...@@ -11,6 +11,7 @@ import 'package:flutter/widgets.dart'; ...@@ -11,6 +11,7 @@ import 'package:flutter/widgets.dart';
import 'colors.dart'; import 'colors.dart';
import 'constants.dart'; import 'constants.dart';
import 'debug.dart'; import 'debug.dart';
import 'material_state.dart';
import 'shadows.dart'; import 'shadows.dart';
import 'theme.dart'; import 'theme.dart';
import 'theme_data.dart'; import 'theme_data.dart';
...@@ -76,6 +77,7 @@ class Switch extends StatefulWidget { ...@@ -76,6 +77,7 @@ class Switch extends StatefulWidget {
this.onInactiveThumbImageError, this.onInactiveThumbImageError,
this.materialTapTargetSize, this.materialTapTargetSize,
this.dragStartBehavior = DragStartBehavior.start, this.dragStartBehavior = DragStartBehavior.start,
this.mouseCursor,
this.focusColor, this.focusColor,
this.hoverColor, this.hoverColor,
this.focusNode, this.focusNode,
...@@ -109,6 +111,7 @@ class Switch extends StatefulWidget { ...@@ -109,6 +111,7 @@ class Switch extends StatefulWidget {
this.onInactiveThumbImageError, this.onInactiveThumbImageError,
this.materialTapTargetSize, this.materialTapTargetSize,
this.dragStartBehavior = DragStartBehavior.start, this.dragStartBehavior = DragStartBehavior.start,
this.mouseCursor,
this.focusColor, this.focusColor,
this.hoverColor, this.hoverColor,
this.focusNode, this.focusNode,
...@@ -206,6 +209,20 @@ class Switch extends StatefulWidget { ...@@ -206,6 +209,20 @@ class Switch extends StatefulWidget {
/// {@macro flutter.cupertino.switch.dragStartBehavior} /// {@macro flutter.cupertino.switch.dragStartBehavior}
final DragStartBehavior 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. /// The color for the button's [Material] when it has the input focus.
final Color focusColor; final Color focusColor;
...@@ -303,6 +320,15 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin { ...@@ -303,6 +320,15 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin {
inactiveThumbColor = widget.inactiveThumbColor ?? (isDark ? Colors.grey.shade800 : Colors.grey.shade400); inactiveThumbColor = widget.inactiveThumbColor ?? (isDark ? Colors.grey.shade800 : Colors.grey.shade400);
inactiveTrackColor = widget.inactiveTrackColor ?? (isDark ? Colors.white10 : Colors.black12); 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( return FocusableActionDetector(
actions: _actionMap, actions: _actionMap,
...@@ -311,6 +337,7 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin { ...@@ -311,6 +337,7 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin {
enabled: enabled, enabled: enabled,
onShowFocusHighlight: _handleFocusHighlightChanged, onShowFocusHighlight: _handleFocusHighlightChanged,
onShowHoverHighlight: _handleHoverChanged, onShowHoverHighlight: _handleHoverChanged,
mouseCursor: effectiveMouseCursor,
child: Builder( child: Builder(
builder: (BuildContext context) { builder: (BuildContext context) {
return _SwitchRenderObjectWidget( return _SwitchRenderObjectWidget(
......
...@@ -611,6 +611,7 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { ...@@ -611,6 +611,7 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
this.unselectedLabelColor, this.unselectedLabelColor,
this.unselectedLabelStyle, this.unselectedLabelStyle,
this.dragStartBehavior = DragStartBehavior.start, this.dragStartBehavior = DragStartBehavior.start,
this.mouseCursor,
this.onTap, this.onTap,
}) : assert(tabs != null), }) : assert(tabs != null),
assert(isScrollable != null), assert(isScrollable != null),
...@@ -734,6 +735,12 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { ...@@ -734,6 +735,12 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
/// {@macro flutter.widgets.scrollable.dragStartBehavior} /// {@macro flutter.widgets.scrollable.dragStartBehavior}
final DragStartBehavior 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. /// 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. /// The callback is applied to the index of the tab where the tap occurred.
...@@ -1067,6 +1074,7 @@ class _TabBarState extends State<TabBar> { ...@@ -1067,6 +1074,7 @@ class _TabBarState extends State<TabBar> {
final int tabCount = widget.tabs.length; final int tabCount = widget.tabs.length;
for (int index = 0; index < tabCount; index += 1) { for (int index = 0; index < tabCount; index += 1) {
wrappedTabs[index] = InkWell( wrappedTabs[index] = InkWell(
mouseCursor: widget.mouseCursor ?? SystemMouseCursors.click,
onTap: () { _handleTap(index); }, onTap: () { _handleTap(index); },
child: Padding( child: Padding(
padding: EdgeInsets.only(bottom: widget.indicatorWeight), padding: EdgeInsets.only(bottom: widget.indicatorWeight),
......
...@@ -1087,6 +1087,7 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe ...@@ -1087,6 +1087,7 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe
onSelectionHandleTapped: _handleSelectionHandleTapped, onSelectionHandleTapped: _handleSelectionHandleTapped,
inputFormatters: formatters, inputFormatters: formatters,
rendererIgnoresPointer: true, rendererIgnoresPointer: true,
mouseCursor: MouseCursor.defer, // TextField will handle the cursor
cursorWidth: widget.cursorWidth, cursorWidth: widget.cursorWidth,
cursorRadius: cursorRadius, cursorRadius: cursorRadius,
cursorColor: cursorColor, cursorColor: cursorColor,
...@@ -1129,6 +1130,7 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe ...@@ -1129,6 +1130,7 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe
return IgnorePointer( return IgnorePointer(
ignoring: !_isEnabled, ignoring: !_isEnabled,
child: MouseRegion( child: MouseRegion(
cursor: SystemMouseCursors.text,
onEnter: (PointerEnterEvent event) => _handleHover(true), onEnter: (PointerEnterEvent event) => _handleHover(true),
onExit: (PointerExitEvent event) => _handleHover(false), onExit: (PointerExitEvent event) => _handleHover(false),
child: AnimatedBuilder( child: AnimatedBuilder(
......
...@@ -166,6 +166,7 @@ class ToggleButtons extends StatelessWidget { ...@@ -166,6 +166,7 @@ class ToggleButtons extends StatelessWidget {
@required this.children, @required this.children,
@required this.isSelected, @required this.isSelected,
this.onPressed, this.onPressed,
this.mouseCursor,
this.textStyle, this.textStyle,
this.constraints, this.constraints,
this.color, this.color,
...@@ -218,6 +219,9 @@ class ToggleButtons extends StatelessWidget { ...@@ -218,6 +219,9 @@ class ToggleButtons extends StatelessWidget {
/// When the callback is null, all toggle buttons will be disabled. /// When the callback is null, all toggle buttons will be disabled.
final void Function(int index) onPressed; 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. /// The [TextStyle] to apply to any text in these toggle buttons.
/// ///
/// [TextStyle.color] will be ignored and substituted by [color], /// [TextStyle.color] will be ignored and substituted by [color],
...@@ -601,6 +605,7 @@ class ToggleButtons extends StatelessWidget { ...@@ -601,6 +605,7 @@ class ToggleButtons extends StatelessWidget {
onPressed: onPressed != null onPressed: onPressed != null
? () { onPressed(index); } ? () { onPressed(index); }
: null, : null,
mouseCursor: mouseCursor,
leadingBorderSide: leadingBorderSide, leadingBorderSide: leadingBorderSide,
horizontalBorderSide: horizontalBorderSide, horizontalBorderSide: horizontalBorderSide,
trailingBorderSide: trailingBorderSide, trailingBorderSide: trailingBorderSide,
...@@ -667,6 +672,7 @@ class _ToggleButton extends StatelessWidget { ...@@ -667,6 +672,7 @@ class _ToggleButton extends StatelessWidget {
this.splashColor, this.splashColor,
this.focusNode, this.focusNode,
this.onPressed, this.onPressed,
this.mouseCursor,
this.leadingBorderSide, this.leadingBorderSide,
this.horizontalBorderSide, this.horizontalBorderSide,
this.trailingBorderSide, this.trailingBorderSide,
...@@ -726,6 +732,9 @@ class _ToggleButton extends StatelessWidget { ...@@ -726,6 +732,9 @@ class _ToggleButton extends StatelessWidget {
/// If this is null, the button will be disabled, see [enabled]. /// If this is null, the button will be disabled, see [enabled].
final VoidCallback onPressed; final VoidCallback onPressed;
/// {@macro flutter.material.button.mouseCursor}
final MouseCursor mouseCursor;
/// The width and color of the button's leading side border. /// The width and color of the button's leading side border.
final BorderSide leadingBorderSide; final BorderSide leadingBorderSide;
...@@ -821,6 +830,7 @@ class _ToggleButton extends StatelessWidget { ...@@ -821,6 +830,7 @@ class _ToggleButton extends StatelessWidget {
focusNode: focusNode, focusNode: focusNode,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
onPressed: onPressed, onPressed: onPressed,
mouseCursor: mouseCursor,
child: child, child: child,
), ),
); );
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'basic.dart'; import 'basic.dart';
import 'focus_manager.dart'; import 'focus_manager.dart';
...@@ -869,7 +870,7 @@ class _ActionsMarker extends InheritedWidget { ...@@ -869,7 +870,7 @@ class _ActionsMarker extends InheritedWidget {
class FocusableActionDetector extends StatefulWidget { class FocusableActionDetector extends StatefulWidget {
/// Create a const [FocusableActionDetector]. /// 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({ const FocusableActionDetector({
Key key, Key key,
this.enabled = true, this.enabled = true,
...@@ -880,9 +881,11 @@ class FocusableActionDetector extends StatefulWidget { ...@@ -880,9 +881,11 @@ class FocusableActionDetector extends StatefulWidget {
this.onShowFocusHighlight, this.onShowFocusHighlight,
this.onShowHoverHighlight, this.onShowHoverHighlight,
this.onFocusChange, this.onFocusChange,
this.mouseCursor = MouseCursor.defer,
@required this.child, @required this.child,
}) : assert(enabled != null), }) : assert(enabled != null),
assert(autofocus != null), assert(autofocus != null),
assert(mouseCursor != null),
assert(child != null), assert(child != null),
super(key: key); super(key: key);
...@@ -923,6 +926,13 @@ class FocusableActionDetector extends StatefulWidget { ...@@ -923,6 +926,13 @@ class FocusableActionDetector extends StatefulWidget {
/// Called with true if the [focusNode] has primary focus. /// Called with true if the [focusNode] has primary focus.
final ValueChanged<bool> onFocusChange; 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. /// The child widget for this [FocusableActionDetector] widget.
/// ///
/// {@macro flutter.widgets.child} /// {@macro flutter.widgets.child}
...@@ -1073,6 +1083,7 @@ class _FocusableActionDetectorState extends State<FocusableActionDetector> { ...@@ -1073,6 +1083,7 @@ class _FocusableActionDetectorState extends State<FocusableActionDetector> {
Widget child = MouseRegion( Widget child = MouseRegion(
onEnter: _handleMouseEnter, onEnter: _handleMouseEnter,
onExit: _handleMouseExit, onExit: _handleMouseExit,
cursor: widget.mouseCursor,
child: Focus( child: Focus(
focusNode: widget.focusNode, focusNode: widget.focusNode,
autofocus: widget.autofocus, autofocus: widget.autofocus,
......
...@@ -390,6 +390,7 @@ class EditableText extends StatefulWidget { ...@@ -390,6 +390,7 @@ class EditableText extends StatefulWidget {
this.onSelectionChanged, this.onSelectionChanged,
this.onSelectionHandleTapped, this.onSelectionHandleTapped,
List<TextInputFormatter> inputFormatters, List<TextInputFormatter> inputFormatters,
this.mouseCursor,
this.rendererIgnoresPointer = false, this.rendererIgnoresPointer = false,
this.cursorWidth = 2.0, this.cursorWidth = 2.0,
this.cursorRadius, this.cursorRadius,
...@@ -979,6 +980,16 @@ class EditableText extends StatefulWidget { ...@@ -979,6 +980,16 @@ class EditableText extends StatefulWidget {
/// {@endtemplate} /// {@endtemplate}
final List<TextInputFormatter> inputFormatters; 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 /// If true, the [RenderEditable] created by this widget will not handle
/// pointer events, see [renderEditable] and [RenderEditable.ignorePointer]. /// pointer events, see [renderEditable] and [RenderEditable.ignorePointer].
/// ///
...@@ -2018,68 +2029,71 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2018,68 +2029,71 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
super.build(context); // See AutomaticKeepAliveClientMixin. super.build(context); // See AutomaticKeepAliveClientMixin.
final TextSelectionControls controls = widget.selectionControls; final TextSelectionControls controls = widget.selectionControls;
return Scrollable( return MouseRegion(
excludeFromSemantics: true, cursor: widget.mouseCursor ?? SystemMouseCursors.text,
axisDirection: _isMultiline ? AxisDirection.down : AxisDirection.right, child: Scrollable(
controller: _scrollController, excludeFromSemantics: true,
physics: widget.scrollPhysics, axisDirection: _isMultiline ? AxisDirection.down : AxisDirection.right,
dragStartBehavior: widget.dragStartBehavior, controller: _scrollController,
viewportBuilder: (BuildContext context, ViewportOffset offset) { physics: widget.scrollPhysics,
return CompositedTransformTarget( dragStartBehavior: widget.dragStartBehavior,
link: _toolbarLayerLink, viewportBuilder: (BuildContext context, ViewportOffset offset) {
child: Semantics( return CompositedTransformTarget(
onCopy: _semanticsOnCopy(controls), link: _toolbarLayerLink,
onCut: _semanticsOnCut(controls), child: Semantics(
onPaste: _semanticsOnPaste(controls), onCopy: _semanticsOnCopy(controls),
child: _Editable( onCut: _semanticsOnCut(controls),
key: _editableKey, onPaste: _semanticsOnPaste(controls),
startHandleLayerLink: _startHandleLayerLink, child: _Editable(
endHandleLayerLink: _endHandleLayerLink, key: _editableKey,
textSpan: buildTextSpan(), startHandleLayerLink: _startHandleLayerLink,
value: _value, endHandleLayerLink: _endHandleLayerLink,
cursorColor: _cursorColor, textSpan: buildTextSpan(),
backgroundCursorColor: widget.backgroundCursorColor, value: _value,
showCursor: EditableText.debugDeterministicCursor cursorColor: _cursorColor,
? ValueNotifier<bool>(widget.showCursor) backgroundCursorColor: widget.backgroundCursorColor,
: _cursorVisibilityNotifier, showCursor: EditableText.debugDeterministicCursor
forceLine: widget.forceLine, ? ValueNotifier<bool>(widget.showCursor)
readOnly: widget.readOnly, : _cursorVisibilityNotifier,
hasFocus: _hasFocus, forceLine: widget.forceLine,
maxLines: widget.maxLines, readOnly: widget.readOnly,
minLines: widget.minLines, hasFocus: _hasFocus,
expands: widget.expands, maxLines: widget.maxLines,
strutStyle: widget.strutStyle, minLines: widget.minLines,
selectionColor: widget.selectionColor, expands: widget.expands,
textScaleFactor: widget.textScaleFactor ?? MediaQuery.textScaleFactorOf(context), strutStyle: widget.strutStyle,
textAlign: widget.textAlign, selectionColor: widget.selectionColor,
textDirection: _textDirection, textScaleFactor: widget.textScaleFactor ?? MediaQuery.textScaleFactorOf(context),
locale: widget.locale, textAlign: widget.textAlign,
textWidthBasis: widget.textWidthBasis, textDirection: _textDirection,
obscuringCharacter: widget.obscuringCharacter, locale: widget.locale,
obscureText: widget.obscureText, textWidthBasis: widget.textWidthBasis,
autocorrect: widget.autocorrect, obscuringCharacter: widget.obscuringCharacter,
smartDashesType: widget.smartDashesType, obscureText: widget.obscureText,
smartQuotesType: widget.smartQuotesType, autocorrect: widget.autocorrect,
enableSuggestions: widget.enableSuggestions, smartDashesType: widget.smartDashesType,
offset: offset, smartQuotesType: widget.smartQuotesType,
onSelectionChanged: _handleSelectionChanged, enableSuggestions: widget.enableSuggestions,
onCaretChanged: _handleCaretChanged, offset: offset,
rendererIgnoresPointer: widget.rendererIgnoresPointer, onSelectionChanged: _handleSelectionChanged,
cursorWidth: widget.cursorWidth, onCaretChanged: _handleCaretChanged,
cursorRadius: widget.cursorRadius, rendererIgnoresPointer: widget.rendererIgnoresPointer,
cursorOffset: widget.cursorOffset, cursorWidth: widget.cursorWidth,
selectionHeightStyle: widget.selectionHeightStyle, cursorRadius: widget.cursorRadius,
selectionWidthStyle: widget.selectionWidthStyle, cursorOffset: widget.cursorOffset,
paintCursorAboveText: widget.paintCursorAboveText, selectionHeightStyle: widget.selectionHeightStyle,
enableInteractiveSelection: widget.enableInteractiveSelection, selectionWidthStyle: widget.selectionWidthStyle,
textSelectionDelegate: this, paintCursorAboveText: widget.paintCursorAboveText,
devicePixelRatio: _devicePixelRatio, enableInteractiveSelection: widget.enableInteractiveSelection,
promptRectRange: _currentPromptRectRange, textSelectionDelegate: this,
promptRectColor: widget.autocorrectionTextRectColor, devicePixelRatio: _devicePixelRatio,
promptRectRange: _currentPromptRectRange,
promptRectColor: widget.autocorrectionTextRectColor,
),
), ),
), );
); },
}, ),
); );
} }
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'basic.dart'; import 'basic.dart';
import 'container.dart'; import 'container.dart';
...@@ -105,6 +106,7 @@ class ModalBarrier extends StatelessWidget { ...@@ -105,6 +106,7 @@ class ModalBarrier extends StatelessWidget {
label: semanticsDismissible ? semanticsLabel : null, label: semanticsDismissible ? semanticsLabel : null,
textDirection: semanticsDismissible && semanticsLabel != null ? Directionality.of(context) : null, textDirection: semanticsDismissible && semanticsLabel != null ? Directionality.of(context) : null,
child: MouseRegion( child: MouseRegion(
cursor: SystemMouseCursors.basic,
opaque: true, opaque: true,
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints.expand(), constraints: const BoxConstraints.expand(),
......
...@@ -1661,6 +1661,52 @@ void main() { ...@@ -1661,6 +1661,52 @@ void main() {
semantics.dispose(); 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 }) { Widget boilerplate({ Widget bottomNavigationBar, @required TextDirection textDirection }) {
......
...@@ -602,4 +602,120 @@ void main() { ...@@ -602,4 +602,120 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(box.size, equals(const Size(60, 36))); 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()';
} }
...@@ -437,6 +437,78 @@ void main() { ...@@ -437,6 +437,78 @@ void main() {
await gesture.removePointer(); 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 { testWidgets('Does FlatButton work with focus', (WidgetTester tester) async {
const Color focusColor = Color(0xff001122); const Color focusColor = Color(0xff001122);
......
...@@ -744,6 +744,84 @@ void main() { ...@@ -744,6 +744,84 @@ void main() {
); );
}); });
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 { testWidgets('Floating Action Button has no clip by default', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
await tester.pumpWidget( await tester.pumpWidget(
......
...@@ -638,6 +638,49 @@ void main() { ...@@ -638,6 +638,49 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(box.size, equals(const Size(60, 40))); 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 }) { Widget wrap({ Widget child }) {
......
...@@ -189,6 +189,64 @@ void main() { ...@@ -189,6 +189,64 @@ void main() {
expect(inkFeatures, paintsExactlyCountTimes(#rect, 0)); 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', () { group('feedback', () {
FeedbackTester feedback; FeedbackTester feedback;
......
...@@ -1443,4 +1443,67 @@ void main() { ...@@ -1443,4 +1443,67 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(box.size, equals(const Size(800, 44))); 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);
});
} }
...@@ -373,6 +373,59 @@ void main() { ...@@ -373,6 +373,59 @@ void main() {
expect(didLongPressButton, isTrue); 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 // 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. // 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 { testWidgets('MaterialButton with explicit splashColor and highlightColor', (WidgetTester tester) async {
......
...@@ -109,6 +109,75 @@ void main() { ...@@ -109,6 +109,75 @@ void main() {
gesture.removePointer(); 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 { testWidgets('Does OutlineButton work with focus', (WidgetTester tester) async {
const Color focusColor = Color(0xff001122); const Color focusColor = Color(0xff001122);
......
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
import 'dart:ui' show window, SemanticsFlag; import 'dart:ui' show window, SemanticsFlag;
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../widgets/semantics_tester.dart'; import '../widgets/semantics_tester.dart';
...@@ -1303,6 +1305,86 @@ void main() { ...@@ -1303,6 +1305,86 @@ void main() {
expect(find.text('Tap me please!'), findsOneWidget); 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 { class TestApp extends StatefulWidget {
......
...@@ -613,4 +613,85 @@ void main() { ...@@ -613,4 +613,85 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(box.size, equals(const Size(60, 36))); 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);
});
} }
...@@ -431,6 +431,76 @@ void main() { ...@@ -431,6 +431,76 @@ void main() {
await gesture.removePointer(); 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 { testWidgets('Does RaisedButton work with focus', (WidgetTester tester) async {
const Color focusColor = Color(0xff001122); const Color focusColor = Color(0xff001122);
......
...@@ -570,4 +570,57 @@ void main() { ...@@ -570,4 +570,57 @@ void main() {
expect(box.size, equals(const Size(76, 36))); expect(box.size, equals(const Size(76, 36)));
expect(childRect, equals(const Rect.fromLTRB(372.0, 293.0, 428.0, 307.0))); 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);
});
} }
...@@ -2136,6 +2136,82 @@ void main() { ...@@ -2136,6 +2136,82 @@ void main() {
expect(renderObject.size.height, 200); 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 { testWidgets('Slider implements debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
......
...@@ -912,4 +912,105 @@ void main() { ...@@ -912,4 +912,105 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(value, isTrue); 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();
});
} }
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
...@@ -2028,6 +2029,45 @@ void main() { ...@@ -2028,6 +2029,45 @@ void main() {
expect(() => Tab(text: 'foo', child: Container()), throwsAssertionError); 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 { testWidgets('TabController changes', (WidgetTester tester) async {
// This is a regression test for https://github.com/flutter/flutter/issues/14812 // This is a regression test for https://github.com/flutter/flutter/issues/14812
......
...@@ -7788,4 +7788,31 @@ void main() { ...@@ -7788,4 +7788,31 @@ void main() {
expect(triedToReadClipboard, true); 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);
});
} }
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart'; import '../rendering/mock_canvas.dart';
...@@ -1434,4 +1435,74 @@ void main() { ...@@ -1434,4 +1435,74 @@ void main() {
); );
}, },
); );
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);
});
} }
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -317,6 +318,109 @@ void main() { ...@@ -317,6 +318,109 @@ void main() {
expect(() => Actions.find<DoNothingIntent>(containerKey.currentContext), throwsAssertionError); expect(() => Actions.find<DoNothingIntent>(containerKey.currentContext), throwsAssertionError);
expect(Actions.find<DoNothingIntent>(containerKey.currentContext, nullOk: true), isNull); 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', () { group('Listening', () {
......
...@@ -6,6 +6,7 @@ import 'dart:async'; ...@@ -6,6 +6,7 @@ import 'dart:async';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
...@@ -4702,6 +4703,64 @@ void main() { ...@@ -4702,6 +4703,64 @@ void main() {
state.updateEditingValue(const TextEditingValue(text: '\u{200E}🇧🇼🇧🇷🇮🇴 🇻🇬🇧🇳wahhh!🇧🇬🇧🇫 🇧🇮🇰🇭عَ عَ 🇨🇲 🇨🇦🇮🇨 🇨🇻🇧🇶 🇰🇾🇨🇫 🇹🇩🇨🇱 🇨🇳🇨🇽\u{200F}')); state.updateEditingValue(const TextEditingValue(text: '\u{200E}🇧🇼🇧🇷🇮🇴 🇻🇬🇧🇳wahhh!🇧🇬🇧🇫 🇧🇮🇰🇭عَ عَ 🇨🇲 🇨🇦🇮🇨 🇨🇻🇧🇶 🇰🇾🇨🇫 🇹🇩🇨🇱 🇨🇳🇨🇽\u{200F}'));
expect(state.currentTextEditingValue.text, equals('\u{200E}🇧🇼🇧🇷🇮🇴 🇻🇬🇧🇳wahhh!🇧🇬🇧🇫 🇧🇮🇰🇭عَ عَ \u{200F}🇨🇲 🇨🇦🇮🇨 🇨🇻🇧🇶 🇰🇾🇨🇫 🇹🇩🇨🇱 🇨🇳🇨🇽\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 { class MockTextFormatter extends TextInputFormatter {
......
...@@ -373,6 +373,24 @@ void main() { ...@@ -373,6 +373,24 @@ void main() {
semantics.dispose(); 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 { class FirstWidget extends StatelessWidget {
......
...@@ -3844,4 +3844,24 @@ void main() { ...@@ -3844,4 +3844,24 @@ void main() {
// Long press triggers gesture recognizer. // Long press triggers gesture recognizer.
expect(spyLongPress, 1); 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);
});
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment