Skip to content

Commit

Permalink
Add mouse cursor property to CupertinoRadio (#149681)
Browse files Browse the repository at this point in the history
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`.
  • Loading branch information
victorsanni authored Jun 12, 2024
1 parent b1f9d71 commit 3db2ece
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 1 deletion.
34 changes: 33 additions & 1 deletion packages/flutter/lib/src/cupertino/radio.dart
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ class CupertinoRadio<T> extends StatefulWidget {
required this.value,
required this.groupValue,
required this.onChanged,
this.mouseCursor,
this.toggleable = false,
this.activeColor,
this.inactiveColor,
Expand Down Expand Up @@ -121,6 +122,28 @@ class CupertinoRadio<T> extends StatefulWidget {
/// ```
final ValueChanged<T?>? 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<MouseCursor>].
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.
///
Expand Down Expand Up @@ -239,6 +262,15 @@ class _CupertinoRadioState<T> extends State<CupertinoRadio<T>> with TickerProvid

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.
Expand All @@ -258,6 +290,7 @@ class _CupertinoRadioState<T> extends State<CupertinoRadio<T>> with TickerProvid
checked: widget._selected,
selected: accessibilitySelected,
child: buildToggleable(
mouseCursor: effectiveMouseCursor,
focusNode: widget.focusNode,
autofocus: widget.autofocus,
onFocusChange: onFocusChange,
Expand Down Expand Up @@ -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()
Expand Down
1 change: 1 addition & 0 deletions packages/flutter/lib/src/material/radio.dart
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,7 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin, Togg
value: widget.value,
groupValue: widget.groupValue,
onChanged: widget.onChanged,
mouseCursor: widget.mouseCursor,
toggleable: widget.toggleable,
activeColor: widget.activeColor,
focusColor: widget.focusColor,
Expand Down
126 changes: 126 additions & 0 deletions packages/flutter/test/cupertino/radio_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<int>(
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<int>)));
await tester.pump();
await gesture.moveTo(tester.getCenter(find.byType(CupertinoRadio<int>)));
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<int>(
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<int>)));
await tester.pump();

// Test hovered case.
await gesture.moveTo(tester.getCenter(find.byType(CupertinoRadio<int>)));
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<int>(
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<int>(
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<int>)));
await tester.pump();
await gesture.moveTo(tester.getCenter(find.byType(CupertinoRadio<int>)));
expect(
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
kIsWeb ? SystemMouseCursors.click : SystemMouseCursors.basic
);
});
}

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()';
}

0 comments on commit 3db2ece

Please sign in to comment.