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;
import 'dart:math' as math;
import 'package:flutter/widgets.dart';
import 'package:flutter/rendering.dart';
import 'package:vector_math/vector_math_64.dart' show Vector3;
import 'bottom_navigation_bar_theme.dart';
......@@ -187,6 +188,7 @@ class BottomNavigationBar extends StatefulWidget {
this.unselectedLabelStyle,
this.showSelectedLabels = true,
this.showUnselectedLabels,
this.mouseCursor,
}) : assert(items != null),
assert(items.length >= 2),
assert(
......@@ -314,6 +316,12 @@ class BottomNavigationBar extends StatefulWidget {
/// Whether the labels are shown for the unselected [BottomNavigationBarItem]s.
final bool showSelectedLabels;
/// The cursor for a mouse pointer when it enters or is hovering over the
/// tiles.
///
/// If this property is null, [SystemMouseCursors.click] will be used.
final MouseCursor mouseCursor;
@override
_BottomNavigationBarState createState() => _BottomNavigationBarState();
}
......@@ -337,12 +345,14 @@ class _BottomNavigationTile extends StatelessWidget {
this.showSelectedLabels,
this.showUnselectedLabels,
this.indexLabel,
@required this.mouseCursor,
}) : assert(type != null),
assert(item != null),
assert(animation != null),
assert(selected != null),
assert(selectedLabelStyle != null),
assert(unselectedLabelStyle != null);
assert(unselectedLabelStyle != null),
assert(mouseCursor != null);
final BottomNavigationBarType type;
final BottomNavigationBarItem item;
......@@ -359,6 +369,7 @@ class _BottomNavigationTile extends StatelessWidget {
final String indexLabel;
final bool showSelectedLabels;
final bool showUnselectedLabels;
final MouseCursor mouseCursor;
@override
Widget build(BuildContext context) {
......@@ -452,6 +463,7 @@ class _BottomNavigationTile extends StatelessWidget {
children: <Widget>[
InkResponse(
onTap: onTap,
mouseCursor: mouseCursor,
child: Padding(
padding: EdgeInsets.only(top: topPadding, bottom: bottomPadding),
child: Column(
......@@ -833,6 +845,7 @@ class _BottomNavigationBarState extends State<BottomNavigationBar> with TickerPr
);
break;
}
final MouseCursor effectiveMouseCursor = widget.mouseCursor ?? SystemMouseCursors.click;
final List<Widget> tiles = <Widget>[];
for (int i = 0; i < widget.items.length; i++) {
......@@ -855,6 +868,7 @@ class _BottomNavigationBarState extends State<BottomNavigationBar> with TickerPr
showSelectedLabels: widget.showSelectedLabels ?? bottomTheme.showSelectedLabels,
showUnselectedLabels: widget.showUnselectedLabels ?? bottomTheme.showUnselectedLabels ?? _defaultShowUnselected,
indexLabel: localizations.tabLabel(tabIndex: i + 1, tabCount: widget.items.length),
mouseCursor: effectiveMouseCursor,
));
}
return tiles;
......
......@@ -103,9 +103,20 @@ class RawMaterialButton extends StatefulWidget {
/// [State.setState] is not allowed).
final ValueChanged<bool> onHighlightChanged;
/// {@macro flutter.material.inkwell.mousecursor}
/// {@template flutter.material.button.mouseCursor}
/// The cursor for a mouse pointer when it enters or is hovering over the
/// button.
///
/// If [mouseCursor] is a [MaterialStateProperty<MouseCursor>],
/// [MaterialStateProperty.resolve] is used for the following [MaterialState]s:
///
/// If the property is null, [SystemMouseCursor.click] is used.
/// * [MaterialState.pressed].
/// * [MaterialState.hovered].
/// * [MaterialState.focused].
/// * [MaterialState.disabled].
///
/// If this property is null, [MaterialStateMouseCursor.clickable] will be used.
/// {@endtemplate flutter.material.button.mouseCursor}
final MouseCursor mouseCursor;
/// Defines the default text style, with [Material.textStyle], for the
......@@ -373,6 +384,10 @@ class _RawMaterialButtonState extends State<RawMaterialButton> {
final ShapeBorder effectiveShape = MaterialStateProperty.resolveAs<ShapeBorder>(widget.shape, _states);
final Offset densityAdjustment = widget.visualDensity.baseSizeAdjustment;
final BoxConstraints effectiveConstraints = widget.visualDensity.effectiveConstraints(widget.constraints);
final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor>(
widget.mouseCursor ?? MaterialStateMouseCursor.clickable,
_states,
);
final EdgeInsetsGeometry padding = widget.padding.add(
EdgeInsets.only(
left: densityAdjustment.dx,
......@@ -382,6 +397,7 @@ class _RawMaterialButtonState extends State<RawMaterialButton> {
),
).clamp(EdgeInsets.zero, EdgeInsetsGeometry.infinity);
final Widget result = ConstrainedBox(
constraints: effectiveConstraints,
child: Material(
......@@ -407,7 +423,7 @@ class _RawMaterialButtonState extends State<RawMaterialButton> {
onLongPress: widget.onLongPress,
enableFeedback: widget.enableFeedback,
customBorder: effectiveShape,
mouseCursor: widget.mouseCursor,
mouseCursor: effectiveMouseCursor,
child: IconTheme.merge(
data: IconThemeData(color: effectiveTextColor),
child: Container(
......
......@@ -11,6 +11,7 @@ import 'package:flutter/widgets.dart';
import 'constants.dart';
import 'debug.dart';
import 'material_state.dart';
import 'theme.dart';
import 'theme_data.dart';
import 'toggleable.dart';
......@@ -60,6 +61,7 @@ class Checkbox extends StatefulWidget {
@required this.value,
this.tristate = false,
@required this.onChanged,
this.mouseCursor,
this.activeColor,
this.checkColor,
this.focusColor,
......@@ -107,6 +109,23 @@ class Checkbox extends StatefulWidget {
/// ```
final ValueChanged<bool> onChanged;
/// The cursor for a mouse pointer when it enters or is hovering over the
/// widget.
///
/// If [mouseCursor] is a [MaterialStateProperty<MouseCursor>],
/// [MaterialStateProperty.resolve] is used for the following [MaterialState]s:
///
/// * [MaterialState.selected].
/// * [MaterialState.hovered].
/// * [MaterialState.focused].
/// * [MaterialState.disabled].
///
/// When [value] is null and [tristate] is true, [MaterialState.selected] is
/// included as a state.
///
/// If this property is null, [MaterialStateMouseCursor.clickable] will be used.
final MouseCursor mouseCursor;
/// The color to use when this checkbox is checked.
///
/// Defaults to [ThemeData.toggleableActiveColor].
......@@ -226,6 +245,16 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin {
}
size += (widget.visualDensity ?? themeData.visualDensity).baseSizeAdjustment;
final BoxConstraints additionalConstraints = BoxConstraints.tight(size);
final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor>(
widget.mouseCursor ?? MaterialStateMouseCursor.clickable,
<MaterialState>{
if (!enabled) MaterialState.disabled,
if (_hovering) MaterialState.hovered,
if (_focused) MaterialState.focused,
if (widget.tristate || widget.value) MaterialState.selected,
},
);
return FocusableActionDetector(
actions: _actionMap,
focusNode: widget.focusNode,
......@@ -233,6 +262,7 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin {
enabled: enabled,
onShowFocusHighlight: _handleFocusHighlightChanged,
onShowHoverHighlight: _handleHoverChanged,
mouseCursor: effectiveMouseCursor,
child: Builder(
builder: (BuildContext context) {
return _CheckboxRenderObjectWidget(
......@@ -309,8 +339,10 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
@override
void updateRenderObject(BuildContext context, _RenderCheckbox renderObject) {
renderObject
..value = value
// The `tristate` must be changed before `value` due to the assertion at
// the beginning of `set value`.
..tristate = tristate
..value = value
..activeColor = activeColor
..checkColor = checkColor
..inactiveColor = inactiveColor
......
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'button.dart';
......@@ -104,6 +105,7 @@ class FlatButton extends MaterialButton {
@required VoidCallback onPressed,
VoidCallback onLongPress,
ValueChanged<bool> onHighlightChanged,
MouseCursor mouseCursor,
ButtonTextTheme textTheme,
Color textColor,
Color disabledTextColor,
......@@ -129,6 +131,7 @@ class FlatButton extends MaterialButton {
onPressed: onPressed,
onLongPress: onLongPress,
onHighlightChanged: onHighlightChanged,
mouseCursor: mouseCursor,
textTheme: textTheme,
textColor: textColor,
disabledTextColor: disabledTextColor,
......@@ -161,6 +164,7 @@ class FlatButton extends MaterialButton {
@required VoidCallback onPressed,
VoidCallback onLongPress,
ValueChanged<bool> onHighlightChanged,
MouseCursor mouseCursor,
ButtonTextTheme textTheme,
Color textColor,
Color disabledTextColor,
......@@ -189,6 +193,7 @@ class FlatButton extends MaterialButton {
onPressed: onPressed,
onLongPress: onLongPress,
onHighlightChanged: onHighlightChanged,
mouseCursor: mouseCursor,
fillColor: buttonTheme.getFillColor(this),
textStyle: theme.textTheme.button.copyWith(color: buttonTheme.getTextColor(this)),
focusColor: buttonTheme.getFocusColor(this),
......@@ -224,6 +229,7 @@ class _FlatButtonWithIcon extends FlatButton with MaterialButtonWithIconMixin {
@required VoidCallback onPressed,
VoidCallback onLongPress,
ValueChanged<bool> onHighlightChanged,
MouseCursor mouseCursor,
ButtonTextTheme textTheme,
Color textColor,
Color disabledTextColor,
......@@ -251,6 +257,7 @@ class _FlatButtonWithIcon extends FlatButton with MaterialButtonWithIconMixin {
onPressed: onPressed,
onLongPress: onLongPress,
onHighlightChanged: onHighlightChanged,
mouseCursor: mouseCursor,
textTheme: textTheme,
textColor: textColor,
disabledTextColor: disabledTextColor,
......
......@@ -142,9 +142,9 @@ class FloatingActionButton extends StatelessWidget {
this.highlightElevation,
this.disabledElevation,
@required this.onPressed,
this.mouseCursor,
this.mini = false,
this.shape,
this.mouseCursor,
this.clipBehavior = Clip.none,
this.focusNode,
this.autofocus = false,
......@@ -183,8 +183,8 @@ class FloatingActionButton extends StatelessWidget {
this.highlightElevation,
this.disabledElevation,
@required this.onPressed,
this.mouseCursor = SystemMouseCursors.click,
this.shape,
this.mouseCursor,
this.isExtended = true,
this.materialTapTargetSize,
this.clipBehavior = Clip.none,
......@@ -290,6 +290,9 @@ class FloatingActionButton extends StatelessWidget {
/// If this is set to null, the button will be disabled.
final VoidCallback onPressed;
/// {@macro flutter.material.button.mouseCursor}
final MouseCursor mouseCursor;
/// The z-coordinate at which to place this button relative to its parent.
///
/// This controls the size of the shadow below the floating action button.
......@@ -378,11 +381,6 @@ class FloatingActionButton extends StatelessWidget {
/// shape as well.
final ShapeBorder shape;
/// {@macro flutter.material.inkwell.mousecursor}
///
/// If the property is null, [SystemMouseCursor.click] is used.
final MouseCursor mouseCursor;
/// {@macro flutter.widgets.Clip}
///
/// Defaults to [Clip.none], and must not be null.
......
......@@ -5,6 +5,7 @@
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'constants.dart';
......@@ -151,6 +152,7 @@ class IconButton extends StatelessWidget {
this.splashColor,
this.disabledColor,
@required this.onPressed,
this.mouseCursor = SystemMouseCursors.click,
this.focusNode,
this.autofocus = false,
this.tooltip,
......@@ -274,6 +276,11 @@ class IconButton extends StatelessWidget {
/// If this is set to null, the button will be disabled.
final VoidCallback onPressed;
/// {@macro flutter.material.inkwell.mousecursor}
///
/// Defaults to [SystemMouseCursors.click].
final MouseCursor mouseCursor;
/// {@macro flutter.widgets.Focus.focusNode}
final FocusNode focusNode;
......@@ -370,6 +377,7 @@ class IconButton extends StatelessWidget {
autofocus: autofocus,
canRequestFocus: onPressed != null,
onTap: onPressed,
mouseCursor: mouseCursor,
enableFeedback: enableFeedback,
child: result,
focusColor: focusColor ?? theme.focusColor,
......
......@@ -284,8 +284,8 @@ class InkResponse extends StatelessWidget {
///
/// Must have an ancestor [Material] widget in which to cause ink reactions.
///
/// The [containedInkWell], [highlightShape], [enableFeedback], and
/// [excludeFromSemantics] arguments must not be null.
/// The [mouseCursor], [containedInkWell], [highlightShape], [enableFeedback],
/// and [excludeFromSemantics] arguments must not be null.
const InkResponse({
Key key,
this.child,
......@@ -296,7 +296,7 @@ class InkResponse extends StatelessWidget {
this.onLongPress,
this.onHighlightChanged,
this.onHover,
this.mouseCursor,
this.mouseCursor = MouseCursor.defer,
this.containedInkWell = false,
this.highlightShape = BoxShape.circle,
this.radius,
......@@ -313,7 +313,8 @@ class InkResponse extends StatelessWidget {
this.canRequestFocus = true,
this.onFocusChange,
this.autofocus = false,
}) : assert(containedInkWell != null),
}) : assert(mouseCursor != null),
assert(containedInkWell != null),
assert(highlightShape != null),
assert(enableFeedback != null),
assert(excludeFromSemantics != null),
......@@ -363,12 +364,11 @@ class InkResponse extends StatelessWidget {
/// material.
final ValueChanged<bool> onHover;
/// {@template flutter.material.inkwell.mousecursor}
/// The cursor for a mouse pointer when it enters or is hovering over the
/// region.
/// {@endtemplate}
/// widget.
///
/// If the property is null, [SystemMouseCursor.click] is used.
/// The [cursor] defaults to [MouseCursor.defer], deferring the choice of
/// cursor to the next region behing it in hit-test order.
final MouseCursor mouseCursor;
/// Whether this ink response should be clipped its bounds.
......@@ -544,7 +544,7 @@ class InkResponse extends StatelessWidget {
@override
Widget build(BuildContext context) {
final _ParentInkResponseState parentState = _ParentInkResponseProvider.of(context);
return _InnerInkResponse(
return _InkResponseStateWidget(
child: child,
onTap: onTap,
onTapDown: onTapDown,
......@@ -553,7 +553,7 @@ class InkResponse extends StatelessWidget {
onLongPress: onLongPress,
onHighlightChanged: onHighlightChanged,
onHover: onHover,
mouseCursor: mouseCursor ?? SystemMouseCursors.click,
mouseCursor: mouseCursor,
containedInkWell: containedInkWell,
highlightShape: highlightShape,
radius: radius,
......@@ -591,8 +591,8 @@ class InkResponse extends StatelessWidget {
}
}
class _InnerInkResponse extends StatefulWidget {
const _InnerInkResponse({
class _InkResponseStateWidget extends StatefulWidget {
const _InkResponseStateWidget({
this.child,
this.onTap,
this.onTapDown,
......@@ -601,7 +601,7 @@ class _InnerInkResponse extends StatefulWidget {
this.onLongPress,
this.onHighlightChanged,
this.onHover,
this.mouseCursor,
this.mouseCursor = MouseCursor.defer,
this.containedInkWell = false,
this.highlightShape = BoxShape.circle,
this.radius,
......@@ -626,7 +626,8 @@ class _InnerInkResponse extends StatefulWidget {
assert(enableFeedback != null),
assert(excludeFromSemantics != null),
assert(autofocus != null),
assert(canRequestFocus != null);
assert(canRequestFocus != null),
assert(mouseCursor != null);
final Widget child;
final GestureTapCallback onTap;
......@@ -671,6 +672,7 @@ class _InnerInkResponse extends StatefulWidget {
if (onTapCancel != null) 'tap cancel',
];
properties.add(IterableProperty<String>('gestures', gestures, ifEmpty: '<none>'));
properties.add(DiagnosticsProperty<MouseCursor>('mouseCursor', mouseCursor, defaultValue: MouseCursor.defer));
properties.add(DiagnosticsProperty<bool>('containedInkWell', containedInkWell, level: DiagnosticLevel.fine));
properties.add(DiagnosticsProperty<BoxShape>(
'highlightShape',
......@@ -689,8 +691,8 @@ enum _HighlightType {
focus,
}
class _InkResponseState extends State<_InnerInkResponse>
with AutomaticKeepAliveClientMixin<_InnerInkResponse>
class _InkResponseState extends State<_InkResponseStateWidget>
with AutomaticKeepAliveClientMixin<_InkResponseStateWidget>
implements _ParentInkResponseState {
Set<InteractiveInkFeature> _splashes;
InteractiveInkFeature _currentSplash;
......@@ -732,7 +734,7 @@ class _InkResponseState extends State<_InnerInkResponse>
}
@override
void didUpdateWidget(_InnerInkResponse oldWidget) {
void didUpdateWidget(_InkResponseStateWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (_isWidgetEnabled(widget) != _isWidgetEnabled(oldWidget)) {
_handleHoverChange(_hovering);
......@@ -988,7 +990,7 @@ class _InkResponseState extends State<_InnerInkResponse>
super.deactivate();
}
bool _isWidgetEnabled(_InnerInkResponse widget) {
bool _isWidgetEnabled(_InkResponseStateWidget widget) {
return widget.onTap != null || widget.onDoubleTap != null || widget.onLongPress != null;
}
......@@ -1146,8 +1148,8 @@ class InkWell extends InkResponse {
///
/// Must have an ancestor [Material] widget in which to cause ink reactions.
///
/// The [enableFeedback] and [excludeFromSemantics] arguments must not be
/// null.
/// The [mouseCursor], [enableFeedback], and [excludeFromSemantics] arguments
/// must not be null.
const InkWell({
Key key,
Widget child,
......@@ -1158,7 +1160,7 @@ class InkWell extends InkResponse {
GestureTapCancelCallback onTapCancel,
ValueChanged<bool> onHighlightChanged,
ValueChanged<bool> onHover,
MouseCursor mouseCursor,
MouseCursor mouseCursor = MouseCursor.defer,
Color focusColor,
Color hoverColor,
Color highlightColor,
......
......@@ -13,6 +13,7 @@ import 'constants.dart';
import 'debug.dart';
import 'divider.dart';
import 'ink_well.dart';
import 'material_state.dart';
import 'theme.dart';
import 'theme_data.dart';
......@@ -640,6 +641,7 @@ class ListTile extends StatelessWidget {
this.enabled = true,
this.onTap,
this.onLongPress,
this.mouseCursor,
this.selected = false,
this.focusColor,
this.hoverColor,
......@@ -736,6 +738,18 @@ class ListTile extends StatelessWidget {
/// Inoperative if [enabled] is false.
final GestureLongPressCallback onLongPress;
/// The cursor for a mouse pointer when it enters or is hovering over the
/// widget.
///
/// If [mouseCursor] is a [MaterialStateProperty<MouseCursor>],
/// [MaterialStateProperty.resolve] is used for the following [MaterialState]s:
///
/// * [MaterialState.selected].
/// * [MaterialState.disabled].
///
/// If this property is null, [MaterialStateMouseCursor.clickable] will be used.
final MouseCursor mouseCursor;
/// If this tile is also [enabled] then icons and text are rendered with the same color.
///
/// By default the selected color is the theme's primary color. The selected color
......@@ -909,9 +923,18 @@ class ListTile extends StatelessWidget {
?? tileTheme?.contentPadding?.resolve(textDirection)
?? _defaultContentPadding;
final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor>(
mouseCursor ?? MaterialStateMouseCursor.clickable,
<MaterialState>{
if (!enabled) MaterialState.disabled,
if (selected) MaterialState.selected,
},
);
return InkWell(
onTap: enabled ? onTap : null,
onLongPress: enabled ? onLongPress : null,
mouseCursor: effectiveMouseCursor,
canRequestFocus: enabled,
focusNode: focusNode,
focusColor: focusColor,
......
......@@ -53,6 +53,7 @@ class MaterialButton extends StatelessWidget {
@required this.onPressed,
this.onLongPress,
this.onHighlightChanged,
this.mouseCursor,
this.textTheme,
this.textColor,
this.disabledTextColor,
......@@ -115,6 +116,9 @@ class MaterialButton extends StatelessWidget {
/// [State.setState] is not allowed).
final ValueChanged<bool> onHighlightChanged;
/// {@macro flutter.material.button.mouseCursor}
final MouseCursor mouseCursor;
/// Defines the button's base colors, and the defaults for the button's minimum
/// size, internal padding, and shape.
///
......@@ -387,6 +391,7 @@ class MaterialButton extends StatelessWidget {
onLongPress: onLongPress,
enableFeedback: enableFeedback,
onHighlightChanged: onHighlightChanged,
mouseCursor: mouseCursor,
fillColor: buttonTheme.getFillColor(this),
textStyle: theme.textTheme.button.copyWith(color: buttonTheme.getTextColor(this)),
focusColor: focusColor ?? buttonTheme.getFocusColor(this) ?? theme.focusColor,
......
......@@ -4,6 +4,9 @@
import 'dart:ui' show Color;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
/// Interactive states that some of the Material widgets can take on when
/// receiving input from the user.
///
......@@ -178,6 +181,98 @@ class _MaterialStateColor extends MaterialStateColor {
Color resolve(Set<MaterialState> states) => _resolve(states);
}
/// Defines a [MouseCursor] whose value depends on a set of [MaterialState]s which
/// represent the interactive state of a component.
///
/// This kind of [MouseCursor] is useful when the set of interactive actions a
/// widget supports varies with its state. For example, a mouse pointer hovering
/// over a disabled [FlatButton] should not display [SystemMouseCursors.click],
/// since the button is not clickable. To solve this, you can use
/// [MaterialStateMouseCursor] to assign a different cursor (such as
/// [SystemMouseCursors.basic]) when the [FlatButton] is disabled.
///
/// To use a [MaterialStateMouseCursor], you should create a subclass of
/// [MaterialStateMouseCursor] and implement the abstract `resolve` method.
///
/// {@tool snippet}
///
/// In this next example, we see how you can create a `MaterialStateMouseCursor` by
/// extending the abstract class and overriding the `resolve` method.
///
/// ```dart
/// class ButtonCursor extends MaterialStateMouseCursor {
/// const ButtonCursor();
///
/// @override
/// MouseCursor resolve(Set<MaterialState> states) {
/// if (states.contains(MaterialState.disabled)) {
/// return SystemMouseCursors.forbidden;
/// }
/// return SystemMouseCursors.click;
/// }
///
/// @override
/// String get debugDescription => 'ButtonCursor()';
/// }
///
/// class MyFlatButton extends StatelessWidget {
/// @override
/// Widget build(BuildContext context) {
/// return FlatButton(
/// child: Text('FlatButton'),
/// onPressed: () {},
/// mouseCursor: const ButtonCursor(),
/// );
/// }
/// }
/// ```
/// {@end-tool}
///
/// This should only be used as parameters when they are documented to take
/// [MaterialStateMouseCursor], otherwise only the default state will be used.
abstract class MaterialStateMouseCursor extends MouseCursor implements MaterialStateProperty<MouseCursor> {
/// Creates a [MaterialStateMouseCursor].
const MaterialStateMouseCursor();
@protected
@override
MouseCursorSession createSession(int device) {
return resolve(<MaterialState>{}).createSession(device);
}
/// Returns a [MouseCursor] that's to be used when a Material component is in
/// the specified state.
///
/// This method should never return null.
@override
MouseCursor resolve(Set<MaterialState> states);
/// A mouse cursor for clickable material widgets, which resolves differently
/// when the widget is disabled.
///
/// By default this cursor resolves to [SystemMouseCursors.click]. If the widget is
/// disabled, the cursor resolves to [SystemMouseCursors.basic].
///
/// This cursor is the default for many Material widgets.
static const MaterialStateMouseCursor clickable = _ClickableMouseCursor();
}
class _ClickableMouseCursor extends MaterialStateMouseCursor {
const _ClickableMouseCursor();
@override
MouseCursor resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return SystemMouseCursors.basic;
}
return SystemMouseCursors.click;
}
@override
String get debugDescription => 'MaterialStateMouseCursor(clickable)';
}
/// Interface for classes that can return a value of type `T` based on a set of
/// [MaterialState]s.
///
......
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'button_theme.dart';
......@@ -63,6 +64,7 @@ class OutlineButton extends MaterialButton {
Key key,
@required VoidCallback onPressed,
VoidCallback onLongPress,
MouseCursor mouseCursor,
ButtonTextTheme textTheme,
Color textColor,
Color disabledTextColor,
......@@ -89,6 +91,7 @@ class OutlineButton extends MaterialButton {
key: key,
onPressed: onPressed,
onLongPress: onLongPress,
mouseCursor: mouseCursor,
textTheme: textTheme,
textColor: textColor,
disabledTextColor: disabledTextColor,
......@@ -119,6 +122,7 @@ class OutlineButton extends MaterialButton {
Key key,
@required VoidCallback onPressed,
VoidCallback onLongPress,
MouseCursor mouseCursor,
ButtonTextTheme textTheme,
Color textColor,
Color disabledTextColor,
......@@ -178,6 +182,7 @@ class OutlineButton extends MaterialButton {
autofocus: autofocus,
onPressed: onPressed,
onLongPress: onLongPress,
mouseCursor: mouseCursor,
brightness: buttonTheme.getBrightness(this),
textTheme: textTheme,
textColor: buttonTheme.getTextColor(this),
......@@ -218,6 +223,7 @@ class _OutlineButtonWithIcon extends OutlineButton with MaterialButtonWithIconMi
Key key,
@required VoidCallback onPressed,
VoidCallback onLongPress,
MouseCursor mouseCursor,
ButtonTextTheme textTheme,
Color textColor,
Color disabledTextColor,
......@@ -247,6 +253,7 @@ class _OutlineButtonWithIcon extends OutlineButton with MaterialButtonWithIconMi
key: key,
onPressed: onPressed,
onLongPress: onLongPress,
mouseCursor: mouseCursor,
textTheme: textTheme,
textColor: textColor,
disabledTextColor: disabledTextColor,
......@@ -281,6 +288,7 @@ class _OutlineButton extends StatefulWidget {
Key key,
@required this.onPressed,
this.onLongPress,
this.mouseCursor,
this.brightness,
this.textTheme,
this.textColor,
......@@ -309,6 +317,7 @@ class _OutlineButton extends StatefulWidget {
final VoidCallback onPressed;
final VoidCallback onLongPress;
final MouseCursor mouseCursor;
final Brightness brightness;
final ButtonTextTheme textTheme;
final Color textColor;
......@@ -462,6 +471,7 @@ class _OutlineButtonState extends State<_OutlineButton> with SingleTickerProvide
disabledColor: Colors.transparent,
onPressed: widget.onPressed,
onLongPress: widget.onLongPress,
mouseCursor: widget.mouseCursor,
elevation: 0.0,
disabledElevation: 0.0,
focusElevation: 0.0,
......
......@@ -17,6 +17,7 @@ import 'ink_well.dart';
import 'list_tile.dart';
import 'material.dart';
import 'material_localizations.dart';
import 'material_state.dart';
import 'popup_menu_theme.dart';
import 'theme.dart';
import 'tooltip.dart';
......@@ -216,6 +217,7 @@ class PopupMenuItem<T> extends PopupMenuEntry<T> {
this.enabled = true,
this.height = kMinInteractiveDimension,
this.textStyle,
this.mouseCursor,
@required this.child,
}) : assert(enabled != null),
assert(height != null),
......@@ -242,6 +244,17 @@ class PopupMenuItem<T> extends PopupMenuEntry<T> {
/// If [PopupMenuThemeData.textStyle] is also null, then [ThemeData.textTheme.subtitle1] is used.
final TextStyle textStyle;
/// The cursor for a mouse pointer when it enters or is hovering over the
/// widget.
///
/// If [mouseCursor] is a [MaterialStateProperty<MouseCursor>],
/// [MaterialStateProperty.resolve] is used for the following [MaterialState]:
///
/// * [MaterialState.disabled].
///
/// If this property is null, [MaterialStateMouseCursor.clickable] will be used.
final MouseCursor mouseCursor;
/// The widget below this widget in the tree.
///
/// Typically a single-line [ListTile] (for menus with icons) or a [Text]. An
......@@ -320,10 +333,17 @@ class PopupMenuItemState<T, W extends PopupMenuItem<T>> extends State<W> {
child: item,
);
}
final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor>(
widget.mouseCursor ?? MaterialStateMouseCursor.clickable,
<MaterialState>{
if (!widget.enabled) MaterialState.disabled,
},
);
return InkWell(
onTap: widget.enabled ? handleTap : null,
canRequestFocus: widget.enabled,
mouseCursor: effectiveMouseCursor,
child: item,
);
}
......
......@@ -9,6 +9,7 @@ import 'package:flutter/widgets.dart';
import 'constants.dart';
import 'debug.dart';
import 'material_state.dart';
import 'theme.dart';
import 'theme_data.dart';
import 'toggleable.dart';
......@@ -108,6 +109,7 @@ class Radio<T> extends StatefulWidget {
@required this.value,
@required this.groupValue,
@required this.onChanged,
this.mouseCursor,
this.toggleable = false,
this.activeColor,
this.focusColor,
......@@ -157,6 +159,20 @@ class Radio<T> extends StatefulWidget {
/// ```
final ValueChanged<T> onChanged;
/// The cursor for a mouse pointer when it enters or is hovering over the
/// widget.
///
/// If [mouseCursor] is a [MaterialStateProperty<MouseCursor>],
/// [MaterialStateProperty.resolve] is used for the following [MaterialState]s:
///
/// * [MaterialState.selected].
/// * [MaterialState.hovered].
/// * [MaterialState.focused].
/// * [MaterialState.disabled].
///
/// If this property is null, [MaterialStateMouseCursor.clickable] will be used.
final MouseCursor mouseCursor;
/// Set to true if this radio button is allowed to be returned to an
/// indeterminate state by selecting it again when selected.
///
......@@ -325,17 +341,29 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin {
}
size += (widget.visualDensity ?? themeData.visualDensity).baseSizeAdjustment;
final BoxConstraints additionalConstraints = BoxConstraints.tight(size);
final bool selected = widget.value == widget.groupValue;
final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor>(
widget.mouseCursor ?? MaterialStateMouseCursor.clickable,
<MaterialState>{
if (!enabled) MaterialState.disabled,
if (_hovering) MaterialState.hovered,
if (_focused) MaterialState.focused,
if (selected) MaterialState.selected,
},
);
return FocusableActionDetector(
actions: _actionMap,
focusNode: widget.focusNode,
autofocus: widget.autofocus,
mouseCursor: effectiveMouseCursor,
enabled: enabled,
onShowFocusHighlight: _handleHighlightChanged,
onShowHoverHighlight: _handleHoverChanged,
child: Builder(
builder: (BuildContext context) {
return _RadioRenderObjectWidget(
selected: widget.value == widget.groupValue,
selected: selected,
activeColor: widget.activeColor ?? themeData.toggleableActiveColor,
inactiveColor: _getInactiveColor(themeData),
focusColor: widget.focusColor ?? themeData.focusColor,
......
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'button.dart';
......@@ -110,6 +111,7 @@ class RaisedButton extends MaterialButton {
@required VoidCallback onPressed,
VoidCallback onLongPress,
ValueChanged<bool> onHighlightChanged,
MouseCursor mouseCursor,
ButtonTextTheme textTheme,
Color textColor,
Color disabledTextColor,
......@@ -146,6 +148,7 @@ class RaisedButton extends MaterialButton {
onPressed: onPressed,
onLongPress: onLongPress,
onHighlightChanged: onHighlightChanged,
mouseCursor: mouseCursor,
textTheme: textTheme,
textColor: textColor,
disabledTextColor: disabledTextColor,
......@@ -185,6 +188,7 @@ class RaisedButton extends MaterialButton {
@required VoidCallback onPressed,
VoidCallback onLongPress,
ValueChanged<bool> onHighlightChanged,
MouseCursor mouseCursor,
ButtonTextTheme textTheme,
Color textColor,
Color disabledTextColor,
......@@ -217,6 +221,7 @@ class RaisedButton extends MaterialButton {
onPressed: onPressed,
onLongPress: onLongPress,
onHighlightChanged: onHighlightChanged,
mouseCursor: mouseCursor,
clipBehavior: clipBehavior,
fillColor: buttonTheme.getFillColor(this),
textStyle: theme.textTheme.button.copyWith(color: buttonTheme.getTextColor(this)),
......@@ -262,6 +267,7 @@ class _RaisedButtonWithIcon extends RaisedButton with MaterialButtonWithIconMixi
@required VoidCallback onPressed,
VoidCallback onLongPress,
ValueChanged<bool> onHighlightChanged,
MouseCursor mouseCursor,
ButtonTextTheme textTheme,
Color textColor,
Color disabledTextColor,
......@@ -296,6 +302,7 @@ class _RaisedButtonWithIcon extends RaisedButton with MaterialButtonWithIconMixi
onPressed: onPressed,
onLongPress: onLongPress,
onHighlightChanged: onHighlightChanged,
mouseCursor: mouseCursor,
textTheme: textTheme,
textColor: textColor,
disabledTextColor: disabledTextColor,
......
......@@ -17,6 +17,7 @@ import 'package:flutter/widgets.dart';
import 'constants.dart';
import 'debug.dart';
import 'material.dart';
import 'material_state.dart';
import 'slider_theme.dart';
import 'theme.dart';
......@@ -159,6 +160,7 @@ class Slider extends StatefulWidget {
this.label,
this.activeColor,
this.inactiveColor,
this.mouseCursor,
this.semanticFormatterCallback,
this.focusNode,
this.autofocus = false,
......@@ -188,6 +190,7 @@ class Slider extends StatefulWidget {
this.max = 1.0,
this.divisions,
this.label,
this.mouseCursor,
this.activeColor,
this.inactiveColor,
this.semanticFormatterCallback,
......@@ -381,6 +384,19 @@ class Slider extends StatefulWidget {
/// Ignored if this slider is created with [Slider.adaptive].
final Color inactiveColor;
/// The cursor for a mouse pointer when it enters or is hovering over the
/// widget.
///
/// If [mouseCursor] is a [MaterialStateProperty<MouseCursor>],
/// [MaterialStateProperty.resolve] is used for the following [MaterialState]s:
///
/// * [MaterialState.hovered].
/// * [MaterialState.focused].
/// * [MaterialState.disabled].
///
/// If this property is null, [MaterialStateMouseCursor.clickable] will be used.
final MouseCursor mouseCursor;
/// The callback used to create a semantic value from a slider value.
///
/// Defaults to formatting values as a percentage.
......@@ -537,7 +553,7 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
widget.onChangeEnd(_lerp(value));
}
void _actionHandler (_AdjustSliderIntent intent) {
void _actionHandler(_AdjustSliderIntent intent) {
final _RenderSlider renderSlider = _renderObjectKey.currentContext.findRenderObject() as _RenderSlider;
final TextDirection textDirection = Directionality.of(_renderObjectKey.currentContext);
switch (intent.type) {
......@@ -682,6 +698,14 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
color: theme.colorScheme.onPrimary,
),
);
final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor>(
widget.mouseCursor ?? MaterialStateMouseCursor.clickable,
<MaterialState>{
if (!_enabled) MaterialState.disabled,
if (_hovering) MaterialState.hovered,
if (_focused) MaterialState.focused,
},
);
// This size is used as the max bounds for the painting of the value
// indicators It must be kept in sync with the function with the same name
......@@ -696,6 +720,7 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
enabled: _enabled,
onShowFocusHighlight: _handleFocusHighlightChanged,
onShowHoverHighlight: _handleHoverChanged,
mouseCursor: effectiveMouseCursor,
child: CompositedTransformTarget(
link: _layerLink,
child: _SliderRenderObjectWidget(
......
......@@ -11,6 +11,7 @@ import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'constants.dart';
import 'debug.dart';
import 'material_state.dart';
import 'shadows.dart';
import 'theme.dart';
import 'theme_data.dart';
......@@ -76,6 +77,7 @@ class Switch extends StatefulWidget {
this.onInactiveThumbImageError,
this.materialTapTargetSize,
this.dragStartBehavior = DragStartBehavior.start,
this.mouseCursor,
this.focusColor,
this.hoverColor,
this.focusNode,
......@@ -109,6 +111,7 @@ class Switch extends StatefulWidget {
this.onInactiveThumbImageError,
this.materialTapTargetSize,
this.dragStartBehavior = DragStartBehavior.start,
this.mouseCursor,
this.focusColor,
this.hoverColor,
this.focusNode,
......@@ -206,6 +209,20 @@ class Switch extends StatefulWidget {
/// {@macro flutter.cupertino.switch.dragStartBehavior}
final DragStartBehavior dragStartBehavior;
/// The cursor for a mouse pointer when it enters or is hovering over the
/// widget.
///
/// If [mouseCursor] is a [MaterialStateProperty<MouseCursor>],
/// [MaterialStateProperty.resolve] is used for the following [MaterialState]s:
///
/// * [MaterialState.selected].
/// * [MaterialState.hovered].
/// * [MaterialState.focused].
/// * [MaterialState.disabled].
///
/// If this property is null, [MaterialStateMouseCursor.clickable] will be used.
final MouseCursor mouseCursor;
/// The color for the button's [Material] when it has the input focus.
final Color focusColor;
......@@ -303,6 +320,15 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin {
inactiveThumbColor = widget.inactiveThumbColor ?? (isDark ? Colors.grey.shade800 : Colors.grey.shade400);
inactiveTrackColor = widget.inactiveTrackColor ?? (isDark ? Colors.white10 : Colors.black12);
}
final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor>(
widget.mouseCursor ?? MaterialStateMouseCursor.clickable,
<MaterialState>{
if (!enabled) MaterialState.disabled,
if (_hovering) MaterialState.hovered,
if (_focused) MaterialState.focused,
if (widget.value) MaterialState.selected,
},
);
return FocusableActionDetector(
actions: _actionMap,
......@@ -311,6 +337,7 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin {
enabled: enabled,
onShowFocusHighlight: _handleFocusHighlightChanged,
onShowHoverHighlight: _handleHoverChanged,
mouseCursor: effectiveMouseCursor,
child: Builder(
builder: (BuildContext context) {
return _SwitchRenderObjectWidget(
......
......@@ -611,6 +611,7 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
this.unselectedLabelColor,
this.unselectedLabelStyle,
this.dragStartBehavior = DragStartBehavior.start,
this.mouseCursor,
this.onTap,
}) : assert(tabs != null),
assert(isScrollable != null),
......@@ -734,6 +735,12 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
final DragStartBehavior dragStartBehavior;
/// The cursor for a mouse pointer when it enters or is hovering over the
/// individual tab widgets.
///
/// If this property is null, [SystemMouseCursors.click] will be used.
final MouseCursor mouseCursor;
/// An optional callback that's called when the [TabBar] is tapped.
///
/// The callback is applied to the index of the tab where the tap occurred.
......@@ -1067,6 +1074,7 @@ class _TabBarState extends State<TabBar> {
final int tabCount = widget.tabs.length;
for (int index = 0; index < tabCount; index += 1) {
wrappedTabs[index] = InkWell(
mouseCursor: widget.mouseCursor ?? SystemMouseCursors.click,
onTap: () { _handleTap(index); },
child: Padding(
padding: EdgeInsets.only(bottom: widget.indicatorWeight),
......
......@@ -1087,6 +1087,7 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe
onSelectionHandleTapped: _handleSelectionHandleTapped,
inputFormatters: formatters,
rendererIgnoresPointer: true,
mouseCursor: MouseCursor.defer, // TextField will handle the cursor
cursorWidth: widget.cursorWidth,
cursorRadius: cursorRadius,
cursorColor: cursorColor,
......@@ -1129,6 +1130,7 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe
return IgnorePointer(
ignoring: !_isEnabled,
child: MouseRegion(
cursor: SystemMouseCursors.text,
onEnter: (PointerEnterEvent event) => _handleHover(true),
onExit: (PointerExitEvent event) => _handleHover(false),
child: AnimatedBuilder(
......
......@@ -166,6 +166,7 @@ class ToggleButtons extends StatelessWidget {
@required this.children,
@required this.isSelected,
this.onPressed,
this.mouseCursor,
this.textStyle,
this.constraints,
this.color,
......@@ -218,6 +219,9 @@ class ToggleButtons extends StatelessWidget {
/// When the callback is null, all toggle buttons will be disabled.
final void Function(int index) onPressed;
/// {@macro flutter.material.button.mouseCursor}
final MouseCursor mouseCursor;
/// The [TextStyle] to apply to any text in these toggle buttons.
///
/// [TextStyle.color] will be ignored and substituted by [color],
......@@ -601,6 +605,7 @@ class ToggleButtons extends StatelessWidget {
onPressed: onPressed != null
? () { onPressed(index); }
: null,
mouseCursor: mouseCursor,
leadingBorderSide: leadingBorderSide,
horizontalBorderSide: horizontalBorderSide,
trailingBorderSide: trailingBorderSide,
......@@ -667,6 +672,7 @@ class _ToggleButton extends StatelessWidget {
this.splashColor,
this.focusNode,
this.onPressed,
this.mouseCursor,
this.leadingBorderSide,
this.horizontalBorderSide,
this.trailingBorderSide,
......@@ -726,6 +732,9 @@ class _ToggleButton extends StatelessWidget {
/// If this is null, the button will be disabled, see [enabled].
final VoidCallback onPressed;
/// {@macro flutter.material.button.mouseCursor}
final MouseCursor mouseCursor;
/// The width and color of the button's leading side border.
final BorderSide leadingBorderSide;
......@@ -821,6 +830,7 @@ class _ToggleButton extends StatelessWidget {
focusNode: focusNode,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
onPressed: onPressed,
mouseCursor: mouseCursor,
child: child,
),
);
......
......@@ -5,6 +5,7 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'basic.dart';
import 'focus_manager.dart';
......@@ -869,7 +870,7 @@ class _ActionsMarker extends InheritedWidget {
class FocusableActionDetector extends StatefulWidget {
/// Create a const [FocusableActionDetector].
///
/// The [enabled], [autofocus], and [child] arguments must not be null.
/// The [enabled], [autofocus], [mouseCursor], and [child] arguments must not be null.
const FocusableActionDetector({
Key key,
this.enabled = true,
......@@ -880,9 +881,11 @@ class FocusableActionDetector extends StatefulWidget {
this.onShowFocusHighlight,
this.onShowHoverHighlight,
this.onFocusChange,
this.mouseCursor = MouseCursor.defer,
@required this.child,
}) : assert(enabled != null),
assert(autofocus != null),
assert(mouseCursor != null),
assert(child != null),
super(key: key);
......@@ -923,6 +926,13 @@ class FocusableActionDetector extends StatefulWidget {
/// Called with true if the [focusNode] has primary focus.
final ValueChanged<bool> onFocusChange;
/// The cursor for a mouse pointer when it enters or is hovering over the
/// widget.
///
/// The [cursor] defaults to [MouseCursor.defer], deferring the choice of
/// cursor to the next region behing it in hit-test order.
final MouseCursor mouseCursor;
/// The child widget for this [FocusableActionDetector] widget.
///
/// {@macro flutter.widgets.child}
......@@ -1073,6 +1083,7 @@ class _FocusableActionDetectorState extends State<FocusableActionDetector> {
Widget child = MouseRegion(
onEnter: _handleMouseEnter,
onExit: _handleMouseExit,
cursor: widget.mouseCursor,
child: Focus(
focusNode: widget.focusNode,
autofocus: widget.autofocus,
......
......@@ -390,6 +390,7 @@ class EditableText extends StatefulWidget {
this.onSelectionChanged,
this.onSelectionHandleTapped,
List<TextInputFormatter> inputFormatters,
this.mouseCursor,
this.rendererIgnoresPointer = false,
this.cursorWidth = 2.0,
this.cursorRadius,
......@@ -979,6 +980,16 @@ class EditableText extends StatefulWidget {
/// {@endtemplate}
final List<TextInputFormatter> inputFormatters;
/// The cursor for a mouse pointer when it enters or is hovering over the
/// widget.
///
/// If this property is null, [SystemMouseCursors.text] will be used.
///
/// The [mouseCursor] is the only property of [EditableText] that controls the
/// mouse pointer. All other properties related to "cursor" stands for the text
/// cursor, which is usually a blinking vertical line at the editing position.
final MouseCursor mouseCursor;
/// If true, the [RenderEditable] created by this widget will not handle
/// pointer events, see [renderEditable] and [RenderEditable.ignorePointer].
///
......@@ -2018,7 +2029,9 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
super.build(context); // See AutomaticKeepAliveClientMixin.
final TextSelectionControls controls = widget.selectionControls;
return Scrollable(
return MouseRegion(
cursor: widget.mouseCursor ?? SystemMouseCursors.text,
child: Scrollable(
excludeFromSemantics: true,
axisDirection: _isMultiline ? AxisDirection.down : AxisDirection.right,
controller: _scrollController,
......@@ -2080,6 +2093,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
),
);
},
),
);
}
......
......@@ -4,6 +4,7 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'basic.dart';
import 'container.dart';
......@@ -105,6 +106,7 @@ class ModalBarrier extends StatelessWidget {
label: semanticsDismissible ? semanticsLabel : null,
textDirection: semanticsDismissible && semanticsLabel != null ? Directionality.of(context) : null,
child: MouseRegion(
cursor: SystemMouseCursors.basic,
opaque: true,
child: ConstrainedBox(
constraints: const BoxConstraints.expand(),
......
......@@ -1661,6 +1661,52 @@ void main() {
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 }) {
......
......@@ -602,4 +602,120 @@ void main() {
await tester.pumpAndSettle();
expect(box.size, equals(const Size(60, 36)));
});
testWidgets('Checkbox changes mouse cursor when hovered', (WidgetTester tester) async {
// Test Checkbox() constructor
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Align(
alignment: Alignment.topLeft,
child: Material(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: Checkbox(
mouseCursor: SystemMouseCursors.text,
value: true,
onChanged: (_) {},
),
),
),
),
),
),
);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
await gesture.addPointer(location: tester.getCenter(find.byType(Checkbox)));
addTearDown(gesture.removePointer);
await tester.pump();
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
// Test default cursor
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Align(
alignment: Alignment.topLeft,
child: Material(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: Checkbox(
value: true,
onChanged: (_) {},
),
),
),
),
),
),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
// Test default cursor when disabled
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: Align(
alignment: Alignment.topLeft,
child: Material(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: Checkbox(
value: true,
onChanged: null,
),
),
),
),
),
),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
// Test cursor when tristate
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: Align(
alignment: Alignment.topLeft,
child: Material(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: Checkbox(
value: null,
tristate: true,
onChanged: null,
mouseCursor: _SelectedGrabMouseCursor(),
),
),
),
),
),
),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.grab);
await tester.pumpAndSettle();
});
}
class _SelectedGrabMouseCursor extends MaterialStateMouseCursor {
const _SelectedGrabMouseCursor();
@override
MouseCursor resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
return SystemMouseCursors.grab;
}
return SystemMouseCursors.basic;
}
@override
String get debugDescription => '_SelectedGrabMouseCursor()';
}
......@@ -437,6 +437,78 @@ void main() {
await gesture.removePointer();
});
testWidgets('FlatButton changes mouse cursor when hovered', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: FlatButton.icon(
icon: const Icon(Icons.add),
label: const Text('Hello'),
onPressed: () {},
mouseCursor: SystemMouseCursors.text,
),
),
),
);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
await gesture.addPointer(location: const Offset(1, 1));
addTearDown(gesture.removePointer);
await tester.pump();
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: FlatButton(
onPressed: () {},
mouseCursor: SystemMouseCursors.text,
child: const Text('Hello'),
),
),
),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
// Test default cursor
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: FlatButton(
onPressed: () {},
child: const Text('Hello'),
),
),
),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
// Test default cursor when disabled
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: FlatButton(
onPressed: null,
child: Text('Hello'),
),
),
),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
});
testWidgets('Does FlatButton work with focus', (WidgetTester tester) async {
const Color focusColor = Color(0xff001122);
......
......@@ -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 {
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(
......
......@@ -638,6 +638,49 @@ void main() {
await tester.pumpAndSettle();
expect(box.size, equals(const Size(60, 40)));
});
testWidgets('IconButton.mouseCursor changes cursor on hover', (WidgetTester tester) async {
// Test argument works
await tester.pumpWidget(
Material(
child: Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: IconButton(
onPressed: () {},
mouseCursor: SystemMouseCursors.forbidden,
icon: const Icon(Icons.play_arrow),
),
),
),
),
);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
await gesture.addPointer(location: tester.getCenter(find.byType(IconButton)));
addTearDown(gesture.removePointer);
await tester.pump();
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.forbidden);
// Test default is click
await tester.pumpWidget(
Material(
child: Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: IconButton(
onPressed: () {},
icon: const Icon(Icons.play_arrow),
),
),
),
),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
});
}
Widget wrap({ Widget child }) {
......
......@@ -189,6 +189,64 @@ void main() {
expect(inkFeatures, paintsExactlyCountTimes(#rect, 0));
});
testWidgets('InkWell.mouseCursor changes cursor on hover', (WidgetTester tester) async {
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
await gesture.addPointer(location: const Offset(1, 1));
addTearDown(gesture.removePointer);
// Test argument works
await tester.pumpWidget(
Material(
child: Directionality(
textDirection: TextDirection.ltr,
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: InkWell(
mouseCursor: SystemMouseCursors.click,
onTap: () {},
),
),
),
),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
// Test default of InkWell()
await tester.pumpWidget(
Material(
child: Directionality(
textDirection: TextDirection.ltr,
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: InkWell(
onTap: () {},
),
),
),
),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.forbidden);
// Test default of InkResponse()
await tester.pumpWidget(
Material(
child: Directionality(
textDirection: TextDirection.ltr,
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: InkResponse(
onTap: () {},
),
),
),
),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.forbidden);
});
group('feedback', () {
FeedbackTester feedback;
......
......@@ -1443,4 +1443,67 @@ void main() {
await tester.pumpAndSettle();
expect(box.size, equals(const Size(800, 44)));
});
testWidgets('ListTile changes mouse cursor when hovered', (WidgetTester tester) async {
// Test ListTile() constructor
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: ListTile(
onTap: () {},
mouseCursor: SystemMouseCursors.text,
),
),
),
),
),
);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
await gesture.addPointer(location: tester.getCenter(find.byType(ListTile)));
addTearDown(gesture.removePointer);
await tester.pump();
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
// Test default cursor
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: ListTile(
onTap: () {},
),
),
),
),
),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
// Test default cursor when disabled
await tester.pumpWidget(
const MaterialApp(
home: Material(
child: Center(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: ListTile(
enabled: false,
),
),
),
),
),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
});
}
......@@ -373,6 +373,59 @@ void main() {
expect(didLongPressButton, isTrue);
});
testWidgets('MaterialButton changes mouse cursor when hovered', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: MaterialButton(
onPressed: () {},
mouseCursor: SystemMouseCursors.text,
),
),
),
);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
await gesture.addPointer(location: Offset.zero);
addTearDown(gesture.removePointer);
await tester.pump();
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
// Test default cursor
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: MaterialButton(
onPressed: () {},
),
),
),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
// Test default cursor when disabled
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: MaterialButton(
onPressed: null,
),
),
),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
});
// This test is very similar to the '...explicit splashColor and highlightColor' test
// in icon_button_test.dart. If you change this one, you may want to also change that one.
testWidgets('MaterialButton with explicit splashColor and highlightColor', (WidgetTester tester) async {
......
......@@ -109,6 +109,75 @@ void main() {
gesture.removePointer();
});
testWidgets('OutlineButton changes mouse cursor when hovered', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: OutlineButton.icon(
icon: const Icon(Icons.add),
label: const Text('Hello'),
onPressed: () {},
mouseCursor: SystemMouseCursors.text,
),
),
),
);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
await gesture.addPointer(location: const Offset(1, 1));
addTearDown(gesture.removePointer);
await tester.pump();
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: OutlineButton(
onPressed: () {},
mouseCursor: SystemMouseCursors.text,
),
),
),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
// Test default cursor
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: OutlineButton(
onPressed: () {},
),
),
),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
// Test default cursor when disabled
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: OutlineButton(
onPressed: null,
),
),
),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
});
testWidgets('Does OutlineButton work with focus', (WidgetTester tester) async {
const Color focusColor = Color(0xff001122);
......
......@@ -5,6 +5,8 @@
import 'dart:ui' show window, SemanticsFlag;
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/material.dart';
import '../widgets/semantics_tester.dart';
......@@ -1303,6 +1305,86 @@ void main() {
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 {
......
......@@ -613,4 +613,85 @@ void main() {
await tester.pumpAndSettle();
expect(box.size, equals(const Size(60, 36)));
});
testWidgets('Radio changes mouse cursor when hovered', (WidgetTester tester) async {
const Key key = ValueKey<int>(1);
// Test Radio() constructor
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Align(
alignment: Alignment.topLeft,
child: Material(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: Radio<int>(
key: key,
mouseCursor: SystemMouseCursors.text,
value: 1,
onChanged: (int v) {},
groupValue: 2,
),
),
),
),
),
),
);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
await gesture.addPointer(location: tester.getCenter(find.byKey(key)));
addTearDown(gesture.removePointer);
await tester.pump();
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
// Test default cursor
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Align(
alignment: Alignment.topLeft,
child: Material(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: Radio<int>(
value: 1,
onChanged: (int v) {},
groupValue: 2,
),
),
),
),
),
),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
// Test default cursor when disabled
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: Align(
alignment: Alignment.topLeft,
child: Material(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: Radio<int>(
value: 1,
onChanged: null,
groupValue: 2,
),
),
),
),
),
),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
});
}
......@@ -431,6 +431,76 @@ void main() {
await gesture.removePointer();
});
testWidgets('RaisedButton changes mouse cursor when hovered', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: RaisedButton.icon(
icon: const Icon(Icons.add),
label: const Text('Hello'),
onPressed: () {},
mouseCursor: SystemMouseCursors.text,
),
),
),
);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
await gesture.addPointer(location: const Offset(1, 1));
addTearDown(gesture.removePointer);
await tester.pump();
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: RaisedButton(
onPressed: () {},
mouseCursor: SystemMouseCursors.text,
),
),
),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
// Test default cursor
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: RaisedButton(
onPressed: () {},
),
),
),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
// Test default cursor when disabled
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: RaisedButton(
onPressed: null,
),
),
),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
});
testWidgets('Does RaisedButton work with focus', (WidgetTester tester) async {
const Color focusColor = Color(0xff001122);
......
......@@ -570,4 +570,57 @@ void main() {
expect(box.size, equals(const Size(76, 36)));
expect(childRect, equals(const Rect.fromLTRB(372.0, 293.0, 428.0, 307.0)));
});
testWidgets('RawMaterialButton changes mouse cursor when hovered', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: RawMaterialButton(
onPressed: () {},
mouseCursor: SystemMouseCursors.text,
),
),
),
);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
await gesture.addPointer(location: Offset.zero);
addTearDown(gesture.removePointer);
await tester.pump();
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
// Test default cursor
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: RawMaterialButton(
onPressed: () {},
),
),
),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
// Test default cursor when disabled
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: RawMaterialButton(
onPressed: null,
),
),
),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
});
}
......@@ -2136,6 +2136,82 @@ void main() {
expect(renderObject.size.height, 200);
});
testWidgets('Slider changes mouse cursor when hovered', (WidgetTester tester) async {
// Test Slider() constructor
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: Center(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: Slider(
mouseCursor: SystemMouseCursors.text,
value: 0.5,
onChanged: (double newValue) { },
),
),
),
),
),
)
);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
await gesture.addPointer(location: tester.getCenter(find.byType(Slider)));
addTearDown(gesture.removePointer);
await tester.pump();
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
// Test Slider.adaptive() constructor
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: Center(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: Slider.adaptive(
mouseCursor: SystemMouseCursors.text,
value: 0.5,
onChanged: (double newValue) { },
),
),
),
),
),
)
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
// Test default cursor
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: Center(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: Slider(
value: 0.5,
onChanged: (double newValue) { },
),
),
),
),
),
)
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
});
testWidgets('Slider implements debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
......
......@@ -912,4 +912,105 @@ void main() {
await tester.pumpAndSettle();
expect(value, isTrue);
});
testWidgets('Switch changes mouse cursor when hovered', (WidgetTester tester) async {
// Test Switch.adaptive() constructor
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Align(
alignment: Alignment.topLeft,
child: Material(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: Switch.adaptive(
mouseCursor: SystemMouseCursors.text,
value: true,
onChanged: (_) {},
),
),
),
),
),
),
);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
await gesture.addPointer(location: tester.getCenter(find.byType(Switch)));
addTearDown(gesture.removePointer);
await tester.pump();
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
// Test Switch() constructor
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Align(
alignment: Alignment.topLeft,
child: Material(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: Switch(
mouseCursor: SystemMouseCursors.text,
value: true,
onChanged: (_) {},
),
),
),
),
),
),
);
await gesture.moveTo(tester.getCenter(find.byType(Switch)));
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
// Test default cursor
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Align(
alignment: Alignment.topLeft,
child: Material(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: Switch(
value: true,
onChanged: (_) {},
),
),
),
),
),
),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
// Test default cursor when disabled
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: Align(
alignment: Alignment.topLeft,
child: Material(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: Switch(
value: true,
onChanged: null,
),
),
),
),
),
),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
await tester.pumpAndSettle();
});
}
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/rendering.dart';
......@@ -2028,6 +2029,45 @@ void main() {
expect(() => Tab(text: 'foo', child: Container()), throwsAssertionError);
});
testWidgets('Tabs changes mouse cursor when a tab is hovered', (WidgetTester tester) async {
final List<String> tabs = <String>['A', 'B'];
await tester.pumpWidget(MaterialApp(home: DefaultTabController(
length: tabs.length,
child: Scaffold(
body: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: TabBar(
mouseCursor: SystemMouseCursors.text,
tabs: tabs.map<Widget>((String tab) => Tab(text: tab)).toList(),
),
),
),
),
));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
await gesture.addPointer(location: tester.getCenter(find.byType(Tab).first));
addTearDown(gesture.removePointer);
await tester.pump();
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
// Test default cursor
await tester.pumpWidget(MaterialApp(home: DefaultTabController(
length: tabs.length,
child: Scaffold(
body: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: TabBar(
tabs: tabs.map<Widget>((String tab) => Tab(text: tab)).toList(),
),
),
),
),
));
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
});
testWidgets('TabController changes', (WidgetTester tester) async {
// This is a regression test for https://github.com/flutter/flutter/issues/14812
......
......@@ -7788,4 +7788,31 @@ void main() {
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 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart';
......@@ -1434,4 +1435,74 @@ 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 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
......@@ -317,6 +318,109 @@ void main() {
expect(() => Actions.find<DoNothingIntent>(containerKey.currentContext), throwsAssertionError);
expect(Actions.find<DoNothingIntent>(containerKey.currentContext, nullOk: true), isNull);
});
testWidgets('FocusableActionDetector keeps track of focus and hover even when disabled.', (WidgetTester tester) async {
FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
final GlobalKey containerKey = GlobalKey();
bool invoked = false;
const Intent intent = TestIntent();
final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');
final Action<Intent> testAction = TestAction(
onInvoke: (Intent intent) {
invoked = true;
return invoked;
},
);
bool hovering = false;
bool focusing = false;
Future<void> buildTest(bool enabled) async {
await tester.pumpWidget(
Center(
child: Actions(
dispatcher: TestDispatcher1(postInvoke: collect),
actions: const <Type, Action<Intent>>{},
child: FocusableActionDetector(
enabled: enabled,
focusNode: focusNode,
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.enter): intent,
},
actions: <Type, Action<Intent>>{
TestIntent: testAction,
},
onShowHoverHighlight: (bool value) => hovering = value,
onShowFocusHighlight: (bool value) => focusing = value,
child: Container(width: 100, height: 100, key: containerKey),
),
),
),
);
return tester.pump();
}
await buildTest(true);
focusNode.requestFocus();
await tester.pump();
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await gesture.moveTo(tester.getCenter(find.byKey(containerKey)));
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
expect(hovering, isTrue);
expect(focusing, isTrue);
expect(invoked, isTrue);
invoked = false;
await buildTest(false);
expect(hovering, isFalse);
expect(focusing, isFalse);
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.pump();
expect(invoked, isFalse);
await buildTest(true);
expect(focusing, isFalse);
expect(hovering, isTrue);
await buildTest(false);
expect(focusing, isFalse);
expect(hovering, isFalse);
await gesture.moveTo(Offset.zero);
await buildTest(true);
expect(hovering, isFalse);
expect(focusing, isFalse);
});
testWidgets('FocusableActionDetector changes mouse cursor when hovered', (WidgetTester tester) async {
await tester.pumpWidget(
MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: FocusableActionDetector(
mouseCursor: SystemMouseCursors.text,
onShowHoverHighlight: (_) {},
onShowFocusHighlight: (_) {},
child: Container(),
),
),
);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
await gesture.addPointer(location: const Offset(1, 1));
addTearDown(gesture.removePointer);
await tester.pump();
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
// Test default
await tester.pumpWidget(
MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: FocusableActionDetector(
onShowHoverHighlight: (_) {},
onShowFocusHighlight: (_) {},
child: Container(),
),
),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.forbidden);
});
});
group('Listening', () {
......
......@@ -6,6 +6,7 @@ import 'dart:async';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
......@@ -4702,6 +4703,64 @@ void main() {
state.updateEditingValue(const TextEditingValue(text: '\u{200E}🇧🇼🇧🇷🇮🇴 🇻🇬🇧🇳wahhh!🇧🇬🇧🇫 🇧🇮🇰🇭عَ عَ 🇨🇲 🇨🇦🇮🇨 🇨🇻🇧🇶 🇰🇾🇨🇫 🇹🇩🇨🇱 🇨🇳🇨🇽\u{200F}'));
expect(state.currentTextEditingValue.text, equals('\u{200E}🇧🇼🇧🇷🇮🇴 🇻🇬🇧🇳wahhh!🇧🇬🇧🇫 🇧🇮🇰🇭عَ عَ \u{200F}🇨🇲 🇨🇦🇮🇨 🇨🇻🇧🇶 🇰🇾🇨🇫 🇹🇩🇨🇱 🇨🇳🇨🇽\u{200F}'));
});
testWidgets('EditableText changes mouse cursor when hovered', (WidgetTester tester) async {
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0),
child: Directionality(
textDirection: TextDirection.ltr,
child: FocusScope(
node: focusScopeNode,
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: EditableText(
controller: controller,
backgroundCursorColor: Colors.grey,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
mouseCursor: SystemMouseCursors.click,
),
),
),
),
),
);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
await gesture.addPointer(location: tester.getCenter(find.byType(EditableText)));
addTearDown(gesture.removePointer);
await tester.pump();
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
// Test default cursor
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0),
child: Directionality(
textDirection: TextDirection.ltr,
child: FocusScope(
node: focusScopeNode,
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: EditableText(
controller: controller,
backgroundCursorColor: Colors.grey,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
),
),
),
),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
});
}
class MockTextFormatter extends TextInputFormatter {
......
......@@ -373,6 +373,24 @@ void main() {
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 {
......
......@@ -3844,4 +3844,24 @@ void main() {
// Long press triggers gesture recognizer.
expect(spyLongPress, 1);
});
testWidgets('SelectableText changes mouse cursor when hovered', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Material(
child: Center(
child: SelectableText('test'),
),
),
),
);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
await gesture.addPointer(location: tester.getCenter(find.text('test')));
addTearDown(gesture.removePointer);
await tester.pump();
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
});
}
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