Make `CupertinoRadio`'s `mouseCursor` a `WidgetStateProperty` (#151910)

https://github.com/flutter/flutter/pull/149681 introduced `mouseCursor `to `CupertinoRadio` as a `MouseCursor` instead of a `WidgetStateProperty` to match Material Radio's `mouseCursor` property for `.adaptive`.

This PR changes `mouseCursor` to be of type `WidgetStateProperty<MouseCursor>` as per review comments in https://github.com/flutter/flutter/pull/151788#discussion_r1680538286.

PR bringing `mouseCursor` into `CupertinoRadio`: https://github.com/flutter/flutter/pull/149681.

Part of https://github.com/flutter/flutter/issues/58192
diff --git a/packages/flutter/lib/src/cupertino/radio.dart b/packages/flutter/lib/src/cupertino/radio.dart
index 9ef3b83..39a5dd3 100644
--- a/packages/flutter/lib/src/cupertino/radio.dart
+++ b/packages/flutter/lib/src/cupertino/radio.dart
@@ -132,24 +132,20 @@
   /// 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<MouseCursor>].
-  final MouseCursor? mouseCursor;
+  ///    either a [MouseCursor] or a [WidgetStateProperty].
+  final WidgetStateProperty<MouseCursor>? 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 @@
 
   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<MouseCursor> defaultMouseCursor(Function? onChanged) {
+    final MouseCursor mouseCursor = (onChanged != null && kIsWeb)
+      ? SystemMouseCursors.click
+      : SystemMouseCursors.basic;
+    return WidgetStateProperty.all<MouseCursor>(mouseCursor);
+  }
+
   @override
   State<CupertinoRadio<T>> createState() => _CupertinoRadioState<T>();
 }
@@ -269,15 +277,6 @@
 
     final Color effectiveFillColor = widget.fillColor ?? CupertinoColors.white;
 
-    final WidgetStateProperty<MouseCursor> effectiveMouseCursor =
-      WidgetStateProperty.resolveWith<MouseCursor>((Set<WidgetState> states) {
-        return WidgetStateProperty.resolveAs<MouseCursor?>(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 @@
       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 a574e9c..2dad655 100644
--- a/packages/flutter/lib/src/material/radio.dart
+++ b/packages/flutter/lib/src/material/radio.dart
@@ -447,7 +447,11 @@
               value: widget.value,
               groupValue: widget.groupValue,
               onChanged: widget.onChanged,
-              mouseCursor: widget.mouseCursor,
+              mouseCursor: widget.mouseCursor == null
+                ? CupertinoRadio.defaultMouseCursor(widget.onChanged)
+                : WidgetStateProperty.resolveWith((Set<MaterialState> states) {
+                    return WidgetStateProperty.resolveAs<MouseCursor>(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 f44369a..483fcf1 100644
--- a/packages/flutter/test/cupertino/radio_test.dart
+++ b/packages/flutter/test/cupertino/radio_test.dart
@@ -441,7 +441,7 @@
           value: 1,
           groupValue: 1,
           onChanged: (int? i) { },
-          mouseCursor: SystemMouseCursors.forbidden,
+          mouseCursor: WidgetStateProperty.all(SystemMouseCursors.forbidden),
         ),
       ),
     ));
@@ -463,13 +463,25 @@
     final FocusNode focusNode = FocusNode(debugLabel: 'Radio');
     tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
 
+    MouseCursor getMouseCursor(Set<WidgetState> states) {
+      if (states.contains(WidgetState.disabled)) {
+        return SystemMouseCursors.forbidden;
+      }
+      if (states.contains(WidgetState.focused)) {
+        return SystemMouseCursors.basic;
+      }
+      return SystemMouseCursors.click;
+    }
+
+    final WidgetStateProperty<MouseCursor> mouseCursor = WidgetStateProperty.resolveWith(getMouseCursor);
+
     await tester.pumpWidget(CupertinoApp(
       home: Center(
         child: CupertinoRadio<int>(
           value: 1,
           groupValue: 1,
           onChanged: (int? i) { },
-          mouseCursor: const RadioMouseCursor(),
+          mouseCursor: mouseCursor,
           focusNode: focusNode
         ),
       ),
@@ -498,13 +510,13 @@
     );
 
     // Test disabled case.
-    await tester.pumpWidget(const CupertinoApp(
+    await tester.pumpWidget(CupertinoApp(
       home: Center(
         child: CupertinoRadio<int>(
           value: 1,
           groupValue: 1,
           onChanged: null,
-          mouseCursor: RadioMouseCursor(),
+          mouseCursor: mouseCursor,
         ),
       ),
     ));
@@ -541,21 +553,3 @@
     );
   });
 }
-
-class RadioMouseCursor extends WidgetStateMouseCursor {
-  const RadioMouseCursor();
-
-  @override
-  MouseCursor resolve(Set<WidgetState> 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 c3ede77..e9f4225 100644
--- a/packages/flutter/test/material/radio_test.dart
+++ b/packages/flutter/test/material/radio_test.dart
@@ -1859,6 +1859,46 @@
     }
   });
 
+  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<int>.adaptive(
+            value: 1,
+            groupValue: 1,
+            onChanged: (int? i) {},
+            mouseCursor: mouseCursor,
+          ),
+        ),
+      );
+    }
+
+    for (final TargetPlatform platform in <TargetPlatform>[ 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<int>)));
+      await tester.pump();
+      await gesture.moveTo(tester.getCenter(find.byType(CupertinoRadio<int>)));
+      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 @@
     focusNode.dispose();
   });
 }
+
+class _SelectedGrabMouseCursor extends WidgetStateMouseCursor {
+  const _SelectedGrabMouseCursor();
+
+  @override
+  MouseCursor resolve(Set<WidgetState> states) {
+    if (states.contains(WidgetState.selected)) {
+      return SystemMouseCursors.grab;
+    }
+    return SystemMouseCursors.basic;
+  }
+
+  @override
+  String get debugDescription => '_SelectedGrabMouseCursor()';
+}