diff --git a/packages/flutter/lib/src/cupertino/radio.dart b/packages/flutter/lib/src/cupertino/radio.dart index 9ef3b83e44..39a5dd325a 100644 --- a/packages/flutter/lib/src/cupertino/radio.dart +++ b/packages/flutter/lib/src/cupertino/radio.dart @@ -132,24 +132,20 @@ class CupertinoRadio extends StatefulWidget { /// The cursor for a mouse pointer when it enters or is hovering over the /// widget. /// - /// If [mouseCursor] is a [WidgetStateMouseCursor], - /// [WidgetStateMouseCursor.resolve] is used for the following [WidgetState]s: + /// Resolves in the following states: /// /// * [WidgetState.selected]. - /// * [WidgetState.hovered]. /// * [WidgetState.focused]. /// * [WidgetState.disabled]. /// - /// If null, then [SystemMouseCursors.basic] is used when this radio button is disabled. - /// When this radio button is enabled, [SystemMouseCursors.click] is used on Web, and - /// [SystemMouseCursors.basic] is used on other platforms. + /// Defaults to [defaultMouseCursor]. /// /// 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; + /// either a [MouseCursor] or a [WidgetStateProperty]. + final WidgetStateProperty? mouseCursor; /// Set to true if this radio button is allowed to be returned to an /// indeterminate state by selecting it again when selected. @@ -210,6 +206,18 @@ class CupertinoRadio extends StatefulWidget { bool get _selected => value == groupValue; + /// The default [mouseCursor] of a [CupertinoRadio]. + /// + /// If [onChanged] is null, indicating the radio button is disabled, + /// [SystemMouseCursors.basic] is used. Otherwise, [SystemMouseCursors.click] + /// is used on Web, and [SystemMouseCursors.basic] is used on other platforms. + static WidgetStateProperty defaultMouseCursor(Function? onChanged) { + final MouseCursor mouseCursor = (onChanged != null && kIsWeb) + ? SystemMouseCursors.click + : SystemMouseCursors.basic; + return WidgetStateProperty.all(mouseCursor); + } + @override State> createState() => _CupertinoRadioState(); } @@ -269,15 +277,6 @@ class _CupertinoRadioState extends State> with TickerProvid final Color effectiveFillColor = widget.fillColor ?? CupertinoColors.white; - final WidgetStateProperty effectiveMouseCursor = - WidgetStateProperty.resolveWith((Set states) { - return WidgetStateProperty.resolveAs(widget.mouseCursor, states) - ?? (states.contains(WidgetState.disabled) - ? SystemMouseCursors.basic - : kIsWeb ? SystemMouseCursors.click : SystemMouseCursors.basic - ); - }); - final bool? accessibilitySelected; // Apple devices also use `selected` to annotate radio button's semantics // state. @@ -297,7 +296,7 @@ class _CupertinoRadioState extends State> with TickerProvid checked: widget._selected, selected: accessibilitySelected, child: buildToggleable( - mouseCursor: effectiveMouseCursor, + mouseCursor: widget.mouseCursor ?? CupertinoRadio.defaultMouseCursor(widget.onChanged), focusNode: widget.focusNode, autofocus: widget.autofocus, onFocusChange: onFocusChange, diff --git a/packages/flutter/lib/src/material/radio.dart b/packages/flutter/lib/src/material/radio.dart index a574e9c958..2dad6550c8 100644 --- a/packages/flutter/lib/src/material/radio.dart +++ b/packages/flutter/lib/src/material/radio.dart @@ -447,7 +447,11 @@ class _RadioState extends State> with TickerProviderStateMixin, Togg value: widget.value, groupValue: widget.groupValue, onChanged: widget.onChanged, - mouseCursor: widget.mouseCursor, + mouseCursor: widget.mouseCursor == null + ? CupertinoRadio.defaultMouseCursor(widget.onChanged) + : WidgetStateProperty.resolveWith((Set states) { + return WidgetStateProperty.resolveAs(widget.mouseCursor!, states); + }), toggleable: widget.toggleable, activeColor: widget.activeColor, focusColor: widget.focusColor, diff --git a/packages/flutter/test/cupertino/radio_test.dart b/packages/flutter/test/cupertino/radio_test.dart index f44369afbc..483fcf1f28 100644 --- a/packages/flutter/test/cupertino/radio_test.dart +++ b/packages/flutter/test/cupertino/radio_test.dart @@ -441,7 +441,7 @@ void main() { value: 1, groupValue: 1, onChanged: (int? i) { }, - mouseCursor: SystemMouseCursors.forbidden, + mouseCursor: WidgetStateProperty.all(SystemMouseCursors.forbidden), ), ), )); @@ -463,13 +463,25 @@ void main() { final FocusNode focusNode = FocusNode(debugLabel: 'Radio'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; + MouseCursor getMouseCursor(Set states) { + if (states.contains(WidgetState.disabled)) { + return SystemMouseCursors.forbidden; + } + if (states.contains(WidgetState.focused)) { + return SystemMouseCursors.basic; + } + return SystemMouseCursors.click; + } + + final WidgetStateProperty mouseCursor = WidgetStateProperty.resolveWith(getMouseCursor); + await tester.pumpWidget(CupertinoApp( home: Center( child: CupertinoRadio( value: 1, groupValue: 1, onChanged: (int? i) { }, - mouseCursor: const RadioMouseCursor(), + mouseCursor: mouseCursor, focusNode: focusNode ), ), @@ -498,13 +510,13 @@ void main() { ); // Test disabled case. - await tester.pumpWidget(const CupertinoApp( + await tester.pumpWidget(CupertinoApp( home: Center( child: CupertinoRadio( value: 1, groupValue: 1, onChanged: null, - mouseCursor: RadioMouseCursor(), + mouseCursor: mouseCursor, ), ), )); @@ -541,21 +553,3 @@ void main() { ); }); } - -class RadioMouseCursor extends WidgetStateMouseCursor { - const RadioMouseCursor(); - - @override - MouseCursor resolve(Set states) { - if (states.contains(WidgetState.disabled)) { - return SystemMouseCursors.forbidden; - } - if (states.contains(WidgetState.focused)){ - return SystemMouseCursors.basic; - } - return SystemMouseCursors.click; - } - - @override - String get debugDescription => 'RadioMouseCursor()'; -} diff --git a/packages/flutter/test/material/radio_test.dart b/packages/flutter/test/material/radio_test.dart index c3ede77683..e9f422544d 100644 --- a/packages/flutter/test/material/radio_test.dart +++ b/packages/flutter/test/material/radio_test.dart @@ -1859,6 +1859,46 @@ void main() { } }); + testWidgets('Radio.adaptive respects Radio.mouseCursor', (WidgetTester tester) async { + Widget buildApp({required TargetPlatform platform, MouseCursor? mouseCursor}) { + return MaterialApp( + theme: ThemeData(platform: platform), + home: Material( + child: Radio.adaptive( + value: 1, + groupValue: 1, + onChanged: (int? i) {}, + mouseCursor: mouseCursor, + ), + ), + ); + } + + for (final TargetPlatform platform in [ TargetPlatform.iOS, TargetPlatform.macOS ]) { + await tester.pumpWidget(buildApp(platform: platform)); + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1); + + // Test default mouse cursor. + await gesture.addPointer(location: tester.getCenter(find.byType(CupertinoRadio))); + await tester.pump(); + await gesture.moveTo(tester.getCenter(find.byType(CupertinoRadio))); + expect( + RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), + kIsWeb ? SystemMouseCursors.click : SystemMouseCursors.basic, + ); + + // Test mouse cursor can be configured. + await tester.pumpWidget(buildApp(platform: platform, mouseCursor: SystemMouseCursors.forbidden)); + expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.forbidden); + + // Test Radio.adaptive can resolve a WidgetStateMouseCursor. + await tester.pumpWidget(buildApp(platform: platform, mouseCursor: const _SelectedGrabMouseCursor())); + expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.grab); + + await gesture.removePointer(); + } + }); + testWidgets('Material2 - Radio default overlayColor and fillColor resolves pressed state', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(debugLabel: 'Radio'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; @@ -1993,3 +2033,18 @@ void main() { focusNode.dispose(); }); } + +class _SelectedGrabMouseCursor extends WidgetStateMouseCursor { + const _SelectedGrabMouseCursor(); + + @override + MouseCursor resolve(Set states) { + if (states.contains(WidgetState.selected)) { + return SystemMouseCursors.grab; + } + return SystemMouseCursors.basic; + } + + @override + String get debugDescription => '_SelectedGrabMouseCursor()'; +}