Add mouseCursor property to CupertinoCheckbox (#151788)

Part of https://github.com/flutter/flutter/issues/58192
This commit is contained in:
Victor Sanni 2024-10-02 15:47:18 -07:00 committed by GitHub
parent 896e322fd6
commit d0f2a6887e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 171 additions and 11 deletions

View File

@ -96,6 +96,7 @@ class CupertinoCheckbox extends StatefulWidget {
required this.value, required this.value,
this.tristate = false, this.tristate = false,
required this.onChanged, required this.onChanged,
this.mouseCursor,
this.activeColor, this.activeColor,
@Deprecated( @Deprecated(
'Use fillColor instead. ' 'Use fillColor instead. '
@ -150,6 +151,30 @@ class CupertinoCheckbox extends StatefulWidget {
/// ``` /// ```
final ValueChanged<bool?>? onChanged; final ValueChanged<bool?>? onChanged;
/// The cursor for a mouse pointer when it enters or is hovering over the
/// widget.
///
/// If [mouseCursor] is a [WidgetStateMouseCursor],
/// [WidgetStateProperty.resolve] is used for the following [WidgetState]s:
///
/// * [WidgetState.selected].
/// * [WidgetState.focused].
/// * [WidgetState.disabled].
///
/// When [value] is null and [tristate] is true, [WidgetState.selected] is
/// included as a state.
///
/// If null, then [SystemMouseCursors.basic] is used when this checkbox is
/// disabled. When the checkbox 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;
/// The color to use when this checkbox is checked. /// The color to use when this checkbox is checked.
/// ///
/// If [fillColor] returns a non-null color in the [WidgetState.selected] /// If [fillColor] returns a non-null color in the [WidgetState.selected]
@ -386,11 +411,21 @@ class _CupertinoCheckboxState extends State<CupertinoCheckbox> with TickerProvid
.withSaturation(kCupertinoFocusColorSaturation) .withSaturation(kCupertinoFocusColorSaturation)
.toColor(); .toColor();
final WidgetStateProperty<MouseCursor> effectiveMouseCursor =
WidgetStateProperty.resolveWith<MouseCursor>((Set<WidgetState> states) {
return WidgetStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, states)
?? (kIsWeb && !states.contains(WidgetState.disabled)
? SystemMouseCursors.click
: SystemMouseCursors.basic
);
});
return Semantics( return Semantics(
label: widget.semanticLabel, label: widget.semanticLabel,
checked: widget.value ?? false, checked: widget.value ?? false,
mixed: widget.tristate ? widget.value == null : null, mixed: widget.tristate ? widget.value == null : null,
child: buildToggleable( child: buildToggleable(
mouseCursor: effectiveMouseCursor,
focusNode: widget.focusNode, focusNode: widget.focusNode,
autofocus: widget.autofocus, autofocus: widget.autofocus,
size: const Size.square(kMinInteractiveDimensionCupertino), size: const Size.square(kMinInteractiveDimensionCupertino),

View File

@ -115,7 +115,7 @@ class Checkbox extends StatefulWidget {
/// design [Checkbox]. /// design [Checkbox].
/// ///
/// If a [CupertinoCheckbox] is created, the following parameters are ignored: /// If a [CupertinoCheckbox] is created, the following parameters are ignored:
/// [mouseCursor], [fillColor], [hoverColor], [overlayColor], [splashRadius], /// [fillColor], [hoverColor], [overlayColor], [splashRadius],
/// [materialTapTargetSize], [visualDensity], [isError]. However, [shape] and /// [materialTapTargetSize], [visualDensity], [isError]. However, [shape] and
/// [side] will still affect the [CupertinoCheckbox] and should be handled if /// [side] will still affect the [CupertinoCheckbox] and should be handled if
/// native fidelity is important. /// native fidelity is important.
@ -184,11 +184,10 @@ class Checkbox extends StatefulWidget {
/// The cursor for a mouse pointer when it enters or is hovering over the /// The cursor for a mouse pointer when it enters or is hovering over the
/// widget. /// widget.
/// ///
/// If [mouseCursor] is a [WidgetStateProperty<MouseCursor>], /// If [mouseCursor] is a [WidgetStateMouseCursor],
/// [WidgetStateProperty.resolve] is used for the following [WidgetState]s: /// [WidgetStateProperty.resolve] is used for the following [WidgetState]s:
/// ///
/// * [WidgetState.selected]. /// * [WidgetState.selected].
/// * [WidgetState.hovered].
/// * [WidgetState.focused]. /// * [WidgetState.focused].
/// * [WidgetState.disabled]. /// * [WidgetState.disabled].
/// {@endtemplate} /// {@endtemplate}
@ -202,8 +201,8 @@ class Checkbox extends StatefulWidget {
/// See also: /// See also:
/// ///
/// * [WidgetStateMouseCursor], a [MouseCursor] that implements /// * [WidgetStateMouseCursor], a [MouseCursor] that implements
/// `WidgetStateProperty` which is used in APIs that need to accept /// [WidgetStateProperty] which is used in APIs that need to accept
/// either a [MouseCursor] or a [WidgetStateProperty<MouseCursor>]. /// either a [MouseCursor] or a [WidgetStateProperty].
final MouseCursor? mouseCursor; final MouseCursor? mouseCursor;
/// The color to use when this checkbox is checked. /// The color to use when this checkbox is checked.
@ -496,6 +495,7 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin, Togg
value: value, value: value,
tristate: tristate, tristate: tristate,
onChanged: onChanged, onChanged: onChanged,
mouseCursor: widget.mouseCursor,
activeColor: widget.activeColor, activeColor: widget.activeColor,
checkColor: widget.checkColor, checkColor: widget.checkColor,
focusColor: widget.focusColor, focusColor: widget.focusColor,

View File

@ -7,10 +7,10 @@
// machines. // machines.
@Tags(<String>['reduced-test-set']) @Tags(<String>['reduced-test-set'])
library; library;
import 'dart:ui';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
@ -474,6 +474,80 @@ void main() {
); );
}); });
testWidgets('Checkbox configures mouse cursor', (WidgetTester tester) async {
Widget buildApp({ MouseCursor? mouseCursor, bool enabled = true, bool value = true }) {
return CupertinoApp(
home: Center(
child: CupertinoCheckbox(
value: value,
onChanged: enabled ? (bool? value) {} : null,
mouseCursor: mouseCursor,
),
),
);
}
await tester.pumpWidget(buildApp(value: false));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
addTearDown(gesture.removePointer);
await gesture.addPointer(location: tester.getCenter(find.byType(CupertinoCheckbox)));
await tester.pump();
await gesture.moveTo(tester.getCenter(find.byType(CupertinoCheckbox)));
expect(
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
kIsWeb ? SystemMouseCursors.click : SystemMouseCursors.basic,
);
// Test disabled checkbox.
await tester.pumpWidget(buildApp(enabled: false, value: false));
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
// Test mouse cursor can be configured.
await tester.pumpWidget(buildApp(mouseCursor: SystemMouseCursors.grab));
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.grab);
});
testWidgets('Mouse cursor resolves in selected/focused/disabled states', (WidgetTester tester) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
final FocusNode focusNode = FocusNode(debugLabel: 'Checkbox');
addTearDown(focusNode.dispose);
Widget buildCheckbox({ required bool value, required bool enabled }) {
return CupertinoApp(
home: Center(
child: CupertinoCheckbox(
value: value,
onChanged: enabled ? (bool? value) => true : null,
mouseCursor: const _CheckboxMouseCursor(),
focusNode: focusNode
),
),
);
}
// Test unselected case.
await tester.pumpWidget(buildCheckbox(value: false, enabled: true));
final TestGesture gesture1 = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
addTearDown(gesture1.removePointer);
await gesture1.addPointer(location: tester.getCenter(find.byType(CupertinoCheckbox)));
await tester.pump();
await gesture1.moveTo(tester.getCenter(find.byType(CupertinoCheckbox)));
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
// Test selected case.
await tester.pumpWidget(buildCheckbox(value: true, enabled: true));
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
// Test focused case.
await tester.pumpWidget(buildCheckbox(value: true, enabled: true));
focusNode.requestFocus();
await tester.pump();
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.grab);
// Test disabled case.
await tester.pumpWidget(buildCheckbox(value: true, enabled: false));
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.forbidden);
});
testWidgets('Checkbox default colors, and size in light mode', (WidgetTester tester) async { testWidgets('Checkbox default colors, and size in light mode', (WidgetTester tester) async {
Widget buildCheckbox({bool value = true}) { Widget buildCheckbox({bool value = true}) {
return CupertinoApp( return CupertinoApp(
@ -883,3 +957,21 @@ void main() {
await tester.pump(); await tester.pump();
}); });
} }
class _CheckboxMouseCursor extends WidgetStateMouseCursor {
const _CheckboxMouseCursor();
@override
MouseCursor resolve(Set<WidgetState> states) {
return const WidgetStateProperty<MouseCursor>.fromMap(
<WidgetStatesConstraint, MouseCursor>{
WidgetState.disabled: SystemMouseCursors.forbidden,
WidgetState.focused: SystemMouseCursors.grab,
WidgetState.selected: SystemMouseCursors.click,
WidgetState.any: SystemMouseCursors.basic,
},
).resolve(states);
}
@override
String get debugDescription => '_CheckboxMouseCursor()';
}

View File

@ -823,7 +823,7 @@ void main() {
value: 1, value: 1,
groupValue: 1, groupValue: 1,
onChanged: (int? i) { }, onChanged: (int? i) { },
mouseCursor: const RadioMouseCursor(), mouseCursor: const _RadioMouseCursor(),
focusNode: focusNode focusNode: focusNode
), ),
), ),
@ -858,7 +858,7 @@ void main() {
value: 1, value: 1,
groupValue: 1, groupValue: 1,
onChanged: null, onChanged: null,
mouseCursor: RadioMouseCursor(), mouseCursor: _RadioMouseCursor(),
), ),
), ),
)); ));
@ -896,8 +896,8 @@ void main() {
}); });
} }
class RadioMouseCursor extends WidgetStateMouseCursor { class _RadioMouseCursor extends WidgetStateMouseCursor {
const RadioMouseCursor(); const _RadioMouseCursor();
@override @override
MouseCursor resolve(Set<WidgetState> states) { MouseCursor resolve(Set<WidgetState> states) {
@ -911,5 +911,5 @@ class RadioMouseCursor extends WidgetStateMouseCursor {
} }
@override @override
String get debugDescription => 'RadioMouseCursor()'; String get debugDescription => '_RadioMouseCursor()';
} }

View File

@ -2235,6 +2235,39 @@ void main() {
} }
}); });
testWidgets('Checkbox.adaptive respects Checkbox.mouseCursor on iOS/macOS', (WidgetTester tester) async {
Widget buildApp({ MouseCursor? mouseCursor }) {
return MaterialApp(
home: Material(
child: Checkbox.adaptive(
value: true,
onChanged: (bool? newValue) { },
mouseCursor: mouseCursor,
),
),
);
}
await tester.pumpWidget(buildApp());
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
await gesture.addPointer(location: tester.getCenter(find.byType(CupertinoCheckbox)));
await tester.pump();
await gesture.moveTo(tester.getCenter(find.byType(CupertinoCheckbox)));
expect(
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
kIsWeb ? SystemMouseCursors.click : SystemMouseCursors.basic,
);
// Test mouse cursor can be configured.
await tester.pumpWidget(buildApp(mouseCursor: SystemMouseCursors.click));
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
// Test Checkbox.adaptive can resolve a WidgetStateMouseCursor.
await tester.pumpWidget(buildApp(mouseCursor: const _SelectedGrabMouseCursor()));
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.grab);
await gesture.removePointer();
}, variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.iOS, TargetPlatform.macOS}));
testWidgets('Material2 - Checkbox respects fillColor when it is unchecked', (WidgetTester tester) async { testWidgets('Material2 - Checkbox respects fillColor when it is unchecked', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: false); final ThemeData theme = ThemeData(useMaterial3: false);
const Color activeBackgroundColor = Color(0xff123456); const Color activeBackgroundColor = Color(0xff123456);