diff --git a/packages/flutter/lib/src/cupertino/radio.dart b/packages/flutter/lib/src/cupertino/radio.dart index fcb6d03b7d..8fd59cbe84 100644 --- a/packages/flutter/lib/src/cupertino/radio.dart +++ b/packages/flutter/lib/src/cupertino/radio.dart @@ -71,6 +71,7 @@ class CupertinoRadio extends StatefulWidget { required this.value, required this.groupValue, required this.onChanged, + this.mouseCursor, this.toggleable = false, this.activeColor, this.inactiveColor, @@ -121,6 +122,28 @@ class CupertinoRadio extends StatefulWidget { /// ``` final ValueChanged? onChanged; + /// 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: + /// + /// * [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. + /// + /// 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; + /// Set to true if this radio button is allowed to be returned to an /// indeterminate state by selecting it again when selected. /// @@ -239,6 +262,15 @@ 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. @@ -258,6 +290,7 @@ class _CupertinoRadioState extends State> with TickerProvid checked: widget._selected, selected: accessibilitySelected, child: buildToggleable( + mouseCursor: effectiveMouseCursor, focusNode: widget.focusNode, autofocus: widget.autofocus, onFocusChange: onFocusChange, @@ -309,7 +342,6 @@ class _RadioPainter extends ToggleablePainter { @override void paint(Canvas canvas, Size size) { - final Offset center = (Offset.zero & size).center; final Paint paint = Paint() diff --git a/packages/flutter/lib/src/material/radio.dart b/packages/flutter/lib/src/material/radio.dart index 19bdacf87f..82112f342d 100644 --- a/packages/flutter/lib/src/material/radio.dart +++ b/packages/flutter/lib/src/material/radio.dart @@ -447,6 +447,7 @@ class _RadioState extends State> with TickerProviderStateMixin, Togg value: widget.value, groupValue: widget.groupValue, onChanged: widget.onChanged, + mouseCursor: widget.mouseCursor, 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 c1eb4e6b52..cd30734d41 100644 --- a/packages/flutter/test/cupertino/radio_test.dart +++ b/packages/flutter/test/cupertino/radio_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -428,4 +429,129 @@ void main() { // Release pointer after widget disappeared. await gesture.up(); }); + + testWidgets('Radio configures mouse cursor', (WidgetTester tester) async { + await tester.pumpWidget(CupertinoApp( + home: Center( + child: CupertinoRadio( + value: 1, + groupValue: 1, + onChanged: (int? i) { }, + mouseCursor: SystemMouseCursors.forbidden, + ), + ), + )); + final TestGesture gesture = await tester.createGesture( + kind: PointerDeviceKind.mouse, + pointer: 1 + ); + addTearDown(gesture.removePointer); + 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), + SystemMouseCursors.forbidden + ); + }); + + testWidgets('Mouse cursor resolves in disabled/hovered/focused states', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(debugLabel: 'Radio'); + tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; + + await tester.pumpWidget(CupertinoApp( + home: Center( + child: CupertinoRadio( + value: 1, + groupValue: 1, + onChanged: (int? i) { }, + mouseCursor: const RadioMouseCursor(), + focusNode: focusNode + ), + ), + )); + final TestGesture gesture = await tester.createGesture( + kind: PointerDeviceKind.mouse, + pointer: 1 + ); + addTearDown(gesture.removePointer); + await gesture.addPointer(location: tester.getCenter(find.byType(CupertinoRadio))); + await tester.pump(); + + // Test hovered case. + await gesture.moveTo(tester.getCenter(find.byType(CupertinoRadio))); + expect( + RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), + SystemMouseCursors.click + ); + + // Test focused case. + focusNode.requestFocus(); + await tester.pump(); + expect( + RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), + SystemMouseCursors.basic + ); + + // Test disabled case. + await tester.pumpWidget(const CupertinoApp( + home: Center( + child: CupertinoRadio( + value: 1, + groupValue: 1, + onChanged: null, + mouseCursor: RadioMouseCursor(), + ), + ), + )); + + await tester.pump(); + expect( + RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), + SystemMouseCursors.forbidden + ); + focusNode.dispose(); + }); + + testWidgets('Radio default mouse cursor', (WidgetTester tester) async { + await tester.pumpWidget(CupertinoApp( + home: Center( + child: CupertinoRadio( + value: 1, + groupValue: 1, + onChanged: (int? i) { }, + ), + ), + )); + final TestGesture gesture = await tester.createGesture( + kind: PointerDeviceKind.mouse, + pointer: 1 + ); + addTearDown(gesture.removePointer); + 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 + ); + }); +} + +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()'; }