Skip to content

Commit 3db2ece

Browse files
authored
Add mouse cursor property to CupertinoRadio (#149681)
Adds `mouseCursor` property in `Radio` to `CupertinoRadio` and `Radio.adaptive`. The `mouseCursor` property added is of type `MouseCursor` and not `WidgetStateProperty<MouseCursor>` to match `Radio`'s `mouseCursor`.
1 parent b1f9d71 commit 3db2ece

File tree

3 files changed

+160
-1
lines changed

3 files changed

+160
-1
lines changed

packages/flutter/lib/src/cupertino/radio.dart

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ class CupertinoRadio<T> extends StatefulWidget {
7171
required this.value,
7272
required this.groupValue,
7373
required this.onChanged,
74+
this.mouseCursor,
7475
this.toggleable = false,
7576
this.activeColor,
7677
this.inactiveColor,
@@ -121,6 +122,28 @@ class CupertinoRadio<T> extends StatefulWidget {
121122
/// ```
122123
final ValueChanged<T?>? onChanged;
123124

125+
/// The cursor for a mouse pointer when it enters or is hovering over the
126+
/// widget.
127+
///
128+
/// If [mouseCursor] is a [WidgetStateMouseCursor],
129+
/// [WidgetStateMouseCursor.resolve] is used for the following [WidgetState]s:
130+
///
131+
/// * [WidgetState.selected].
132+
/// * [WidgetState.hovered].
133+
/// * [WidgetState.focused].
134+
/// * [WidgetState.disabled].
135+
///
136+
/// If null, then [SystemMouseCursors.basic] is used when this radio button is disabled.
137+
/// When this radio button is enabled, [SystemMouseCursors.click] is used on Web, and
138+
/// [SystemMouseCursors.basic] is used on other platforms.
139+
///
140+
/// See also:
141+
///
142+
/// * [WidgetStateMouseCursor], a [MouseCursor] that implements
143+
/// `WidgetStateProperty` which is used in APIs that need to accept
144+
/// either a [MouseCursor] or a [WidgetStateProperty<MouseCursor>].
145+
final MouseCursor? mouseCursor;
146+
124147
/// Set to true if this radio button is allowed to be returned to an
125148
/// indeterminate state by selecting it again when selected.
126149
///
@@ -239,6 +262,15 @@ class _CupertinoRadioState<T> extends State<CupertinoRadio<T>> with TickerProvid
239262

240263
final Color effectiveFillColor = widget.fillColor ?? CupertinoColors.white;
241264

265+
final WidgetStateProperty<MouseCursor> effectiveMouseCursor =
266+
WidgetStateProperty.resolveWith<MouseCursor>((Set<WidgetState> states) {
267+
return WidgetStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, states)
268+
?? (states.contains(WidgetState.disabled)
269+
? SystemMouseCursors.basic
270+
: kIsWeb ? SystemMouseCursors.click : SystemMouseCursors.basic
271+
);
272+
});
273+
242274
final bool? accessibilitySelected;
243275
// Apple devices also use `selected` to annotate radio button's semantics
244276
// state.
@@ -258,6 +290,7 @@ class _CupertinoRadioState<T> extends State<CupertinoRadio<T>> with TickerProvid
258290
checked: widget._selected,
259291
selected: accessibilitySelected,
260292
child: buildToggleable(
293+
mouseCursor: effectiveMouseCursor,
261294
focusNode: widget.focusNode,
262295
autofocus: widget.autofocus,
263296
onFocusChange: onFocusChange,
@@ -309,7 +342,6 @@ class _RadioPainter extends ToggleablePainter {
309342

310343
@override
311344
void paint(Canvas canvas, Size size) {
312-
313345
final Offset center = (Offset.zero & size).center;
314346

315347
final Paint paint = Paint()

packages/flutter/lib/src/material/radio.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,7 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin, Togg
447447
value: widget.value,
448448
groupValue: widget.groupValue,
449449
onChanged: widget.onChanged,
450+
mouseCursor: widget.mouseCursor,
450451
toggleable: widget.toggleable,
451452
activeColor: widget.activeColor,
452453
focusColor: widget.focusColor,

packages/flutter/test/cupertino/radio_test.dart

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import 'package:flutter/cupertino.dart';
66
import 'package:flutter/foundation.dart';
7+
import 'package:flutter/gestures.dart';
78
import 'package:flutter/rendering.dart';
89
import 'package:flutter/services.dart';
910
import 'package:flutter_test/flutter_test.dart';
@@ -428,4 +429,129 @@ void main() {
428429
// Release pointer after widget disappeared.
429430
await gesture.up();
430431
});
432+
433+
testWidgets('Radio configures mouse cursor', (WidgetTester tester) async {
434+
await tester.pumpWidget(CupertinoApp(
435+
home: Center(
436+
child: CupertinoRadio<int>(
437+
value: 1,
438+
groupValue: 1,
439+
onChanged: (int? i) { },
440+
mouseCursor: SystemMouseCursors.forbidden,
441+
),
442+
),
443+
));
444+
final TestGesture gesture = await tester.createGesture(
445+
kind: PointerDeviceKind.mouse,
446+
pointer: 1
447+
);
448+
addTearDown(gesture.removePointer);
449+
await gesture.addPointer(location: tester.getCenter(find.byType(CupertinoRadio<int>)));
450+
await tester.pump();
451+
await gesture.moveTo(tester.getCenter(find.byType(CupertinoRadio<int>)));
452+
expect(
453+
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
454+
SystemMouseCursors.forbidden
455+
);
456+
});
457+
458+
testWidgets('Mouse cursor resolves in disabled/hovered/focused states', (WidgetTester tester) async {
459+
final FocusNode focusNode = FocusNode(debugLabel: 'Radio');
460+
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
461+
462+
await tester.pumpWidget(CupertinoApp(
463+
home: Center(
464+
child: CupertinoRadio<int>(
465+
value: 1,
466+
groupValue: 1,
467+
onChanged: (int? i) { },
468+
mouseCursor: const RadioMouseCursor(),
469+
focusNode: focusNode
470+
),
471+
),
472+
));
473+
final TestGesture gesture = await tester.createGesture(
474+
kind: PointerDeviceKind.mouse,
475+
pointer: 1
476+
);
477+
addTearDown(gesture.removePointer);
478+
await gesture.addPointer(location: tester.getCenter(find.byType(CupertinoRadio<int>)));
479+
await tester.pump();
480+
481+
// Test hovered case.
482+
await gesture.moveTo(tester.getCenter(find.byType(CupertinoRadio<int>)));
483+
expect(
484+
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
485+
SystemMouseCursors.click
486+
);
487+
488+
// Test focused case.
489+
focusNode.requestFocus();
490+
await tester.pump();
491+
expect(
492+
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
493+
SystemMouseCursors.basic
494+
);
495+
496+
// Test disabled case.
497+
await tester.pumpWidget(const CupertinoApp(
498+
home: Center(
499+
child: CupertinoRadio<int>(
500+
value: 1,
501+
groupValue: 1,
502+
onChanged: null,
503+
mouseCursor: RadioMouseCursor(),
504+
),
505+
),
506+
));
507+
508+
await tester.pump();
509+
expect(
510+
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
511+
SystemMouseCursors.forbidden
512+
);
513+
focusNode.dispose();
514+
});
515+
516+
testWidgets('Radio default mouse cursor', (WidgetTester tester) async {
517+
await tester.pumpWidget(CupertinoApp(
518+
home: Center(
519+
child: CupertinoRadio<int>(
520+
value: 1,
521+
groupValue: 1,
522+
onChanged: (int? i) { },
523+
),
524+
),
525+
));
526+
final TestGesture gesture = await tester.createGesture(
527+
kind: PointerDeviceKind.mouse,
528+
pointer: 1
529+
);
530+
addTearDown(gesture.removePointer);
531+
await gesture.addPointer(location: tester.getCenter(find.byType(CupertinoRadio<int>)));
532+
await tester.pump();
533+
await gesture.moveTo(tester.getCenter(find.byType(CupertinoRadio<int>)));
534+
expect(
535+
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
536+
kIsWeb ? SystemMouseCursors.click : SystemMouseCursors.basic
537+
);
538+
});
539+
}
540+
541+
class RadioMouseCursor extends WidgetStateMouseCursor {
542+
const RadioMouseCursor();
543+
544+
@override
545+
MouseCursor resolve(Set<WidgetState> states) {
546+
if (states.contains(WidgetState.disabled)) {
547+
return SystemMouseCursors.forbidden;
548+
}
549+
if (states.contains(WidgetState.focused)){
550+
return SystemMouseCursors.basic;
551+
}
552+
return SystemMouseCursors.click;
553+
}
554+
555+
@override
556+
String get debugDescription => 'RadioMouseCursor()';
431557
}

0 commit comments

Comments
 (0)