diff --git a/packages/flutter/lib/src/cupertino/button.dart b/packages/flutter/lib/src/cupertino/button.dart index 6e09a8dd26..25d7018e31 100644 --- a/packages/flutter/lib/src/cupertino/button.dart +++ b/packages/flutter/lib/src/cupertino/button.dart @@ -90,6 +90,7 @@ class CupertinoButton extends StatefulWidget { this.focusNode, this.onFocusChange, this.autofocus = false, + this.mouseCursor, this.onLongPress, required this.onPressed, }) : assert(pressedOpacity == null || (pressedOpacity >= 0.0 && pressedOpacity <= 1.0)), @@ -125,6 +126,7 @@ class CupertinoButton extends StatefulWidget { this.focusNode, this.onFocusChange, this.autofocus = false, + this.mouseCursor, this.onLongPress, required this.onPressed, }) : assert(minimumSize == null || minSize == null), @@ -154,6 +156,7 @@ class CupertinoButton extends StatefulWidget { this.focusNode, this.onFocusChange, this.autofocus = false, + this.mouseCursor, this.onLongPress, required this.onPressed, }) : assert(pressedOpacity == null || (pressedOpacity >= 0.0 && pressedOpacity <= 1.0)), @@ -258,6 +261,23 @@ class CupertinoButton extends StatefulWidget { /// {@macro flutter.widgets.Focus.autofocus} final bool autofocus; + /// The cursor for a mouse pointer when it enters or is hovering over the widget. + /// + /// If [mouseCursor] is a [WidgetStateMouseCursor], + /// [WidgetStateProperty.resolve] is used for the following [WidgetState]: + /// * [WidgetState.disabled]. + /// + /// If null, then [MouseCursor.defer] is used when the button is disabled. + /// When the button is enabled, [SystemMouseCursors.click] is used on Web + /// and [MouseCursor.defer] is used on other platforms. + /// + /// See also: + /// + /// * [WidgetStateMouseCursor], a [MouseCursor] that implements + /// [WidgetStateProperty] which is used in APIs that need to accept + /// either a [MouseCursor] or a [WidgetStateProperty]. + final MouseCursor? mouseCursor; + final _CupertinoButtonStyle _style; /// Whether the button is enabled or disabled. Buttons are disabled by default. To @@ -297,6 +317,13 @@ class _CupertinoButtonState extends State with SingleTickerProv late bool isFocused; + static final WidgetStateProperty _defaultCursor = + WidgetStateProperty.resolveWith((Set states) { + return !states.contains(WidgetState.disabled) && kIsWeb + ? SystemMouseCursors.click + : MouseCursor.defer; + }); + @override void initState() { super.initState(); @@ -459,9 +486,16 @@ class _CupertinoButtonState extends State with SingleTickerProv size: textStyle.fontSize != null ? textStyle.fontSize! * 1.2 : kCupertinoButtonDefaultIconSize, ); + final DeviceGestureSettings? gestureSettings = MediaQuery.maybeGestureSettingsOf(context); + + final Set states = {if (!enabled) WidgetState.disabled}; + final MouseCursor effectiveMouseCursor = + WidgetStateProperty.resolveAs(widget.mouseCursor, states) ?? + _defaultCursor.resolve(states); + return MouseRegion( - cursor: enabled && kIsWeb ? SystemMouseCursors.click : MouseCursor.defer, + cursor: effectiveMouseCursor, child: FocusableActionDetector( actions: _actionMap, focusNode: widget.focusNode, diff --git a/packages/flutter/test/cupertino/button_test.dart b/packages/flutter/test/cupertino/button_test.dart index 5be1c794cb..95990f73dc 100644 --- a/packages/flutter/test/cupertino/button_test.dart +++ b/packages/flutter/test/cupertino/button_test.dart @@ -941,8 +941,63 @@ void main() { await gesture.up(); expect(value, isTrue); }); + + testWidgets('Mouse cursor resolves in enabled/disabled states', (WidgetTester tester) async { + Widget buildButton({required bool enabled, MouseCursor? cursor}) { + return CupertinoApp( + home: Center( + child: CupertinoButton( + onPressed: enabled ? () {} : null, + mouseCursor: cursor, + child: const Text('Tap Me'), + ), + ), + ); + } + + // Test default mouse cursor + final TestGesture gesture = await tester.createGesture( + kind: PointerDeviceKind.mouse, + pointer: 1, + ); + await tester.pumpWidget(buildButton(enabled: true, cursor: const _ButtonMouseCursor())); + await gesture.addPointer(location: tester.getCenter(find.byType(CupertinoButton))); + await tester.pump(); + await gesture.moveTo(tester.getCenter(find.byType(CupertinoButton))); + expect( + RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), + SystemMouseCursors.basic, + ); + await gesture.removePointer(); + + // Test disabled state mouse cursor + await tester.pumpWidget(buildButton(enabled: false, cursor: const _ButtonMouseCursor())); + await gesture.addPointer(location: tester.getCenter(find.byType(CupertinoButton))); + await tester.pump(); + await gesture.moveTo(tester.getCenter(find.byType(CupertinoButton))); + expect( + RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), + SystemMouseCursors.forbidden, + ); + await gesture.removePointer(); + }); } Widget boilerplate({required Widget child}) { return Directionality(textDirection: TextDirection.ltr, child: Center(child: child)); } + +class _ButtonMouseCursor extends WidgetStateMouseCursor { + const _ButtonMouseCursor(); + + @override + MouseCursor resolve(Set states) { + if (states.contains(WidgetState.disabled)) { + return SystemMouseCursors.forbidden; + } + return SystemMouseCursors.basic; + } + + @override + String get debugDescription => '_ButtonMouseCursor()'; +}