diff --git a/packages/flutter/lib/src/cupertino/checkbox.dart b/packages/flutter/lib/src/cupertino/checkbox.dart index ef13df6041..e5896c968a 100644 --- a/packages/flutter/lib/src/cupertino/checkbox.dart +++ b/packages/flutter/lib/src/cupertino/checkbox.dart @@ -96,6 +96,7 @@ class CupertinoCheckbox extends StatefulWidget { required this.value, this.tristate = false, required this.onChanged, + this.mouseCursor, this.activeColor, @Deprecated( 'Use fillColor instead. ' @@ -150,6 +151,30 @@ class CupertinoCheckbox 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], + /// [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. /// /// If [fillColor] returns a non-null color in the [WidgetState.selected] @@ -386,11 +411,21 @@ class _CupertinoCheckboxState extends State with TickerProvid .withSaturation(kCupertinoFocusColorSaturation) .toColor(); + final WidgetStateProperty effectiveMouseCursor = + WidgetStateProperty.resolveWith((Set states) { + return WidgetStateProperty.resolveAs(widget.mouseCursor, states) + ?? (kIsWeb && !states.contains(WidgetState.disabled) + ? SystemMouseCursors.click + : SystemMouseCursors.basic + ); + }); + return Semantics( label: widget.semanticLabel, checked: widget.value ?? false, mixed: widget.tristate ? widget.value == null : null, child: buildToggleable( + mouseCursor: effectiveMouseCursor, focusNode: widget.focusNode, autofocus: widget.autofocus, size: const Size.square(kMinInteractiveDimensionCupertino), diff --git a/packages/flutter/lib/src/material/checkbox.dart b/packages/flutter/lib/src/material/checkbox.dart index 0fe4d70f54..26888516ea 100644 --- a/packages/flutter/lib/src/material/checkbox.dart +++ b/packages/flutter/lib/src/material/checkbox.dart @@ -115,7 +115,7 @@ class Checkbox extends StatefulWidget { /// design [Checkbox]. /// /// 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 /// [side] will still affect the [CupertinoCheckbox] and should be handled if /// 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 /// widget. /// - /// If [mouseCursor] is a [WidgetStateProperty], + /// If [mouseCursor] is a [WidgetStateMouseCursor], /// [WidgetStateProperty.resolve] is used for the following [WidgetState]s: /// /// * [WidgetState.selected]. - /// * [WidgetState.hovered]. /// * [WidgetState.focused]. /// * [WidgetState.disabled]. /// {@endtemplate} @@ -202,8 +201,8 @@ class Checkbox extends StatefulWidget { /// See also: /// /// * [WidgetStateMouseCursor], a [MouseCursor] that implements - /// `WidgetStateProperty` which is used in APIs that need to accept - /// either a [MouseCursor] or a [WidgetStateProperty]. + /// [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. @@ -496,6 +495,7 @@ class _CheckboxState extends State with TickerProviderStateMixin, Togg value: value, tristate: tristate, onChanged: onChanged, + mouseCursor: widget.mouseCursor, activeColor: widget.activeColor, checkColor: widget.checkColor, focusColor: widget.focusColor, diff --git a/packages/flutter/test/cupertino/checkbox_test.dart b/packages/flutter/test/cupertino/checkbox_test.dart index 560747cb3d..8363bce28f 100644 --- a/packages/flutter/test/cupertino/checkbox_test.dart +++ b/packages/flutter/test/cupertino/checkbox_test.dart @@ -7,10 +7,10 @@ // machines. @Tags(['reduced-test-set']) library; -import 'dart:ui'; 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'; @@ -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 { Widget buildCheckbox({bool value = true}) { return CupertinoApp( @@ -883,3 +957,21 @@ void main() { await tester.pump(); }); } + +class _CheckboxMouseCursor extends WidgetStateMouseCursor { + const _CheckboxMouseCursor(); + + @override + MouseCursor resolve(Set states) { + return const WidgetStateProperty.fromMap( + { + WidgetState.disabled: SystemMouseCursors.forbidden, + WidgetState.focused: SystemMouseCursors.grab, + WidgetState.selected: SystemMouseCursors.click, + WidgetState.any: SystemMouseCursors.basic, + }, + ).resolve(states); + } + @override + String get debugDescription => '_CheckboxMouseCursor()'; +} diff --git a/packages/flutter/test/cupertino/radio_test.dart b/packages/flutter/test/cupertino/radio_test.dart index 67b59a0f3f..8692250634 100644 --- a/packages/flutter/test/cupertino/radio_test.dart +++ b/packages/flutter/test/cupertino/radio_test.dart @@ -823,7 +823,7 @@ void main() { value: 1, groupValue: 1, onChanged: (int? i) { }, - mouseCursor: const RadioMouseCursor(), + mouseCursor: const _RadioMouseCursor(), focusNode: focusNode ), ), @@ -858,7 +858,7 @@ void main() { value: 1, groupValue: 1, onChanged: null, - mouseCursor: RadioMouseCursor(), + mouseCursor: _RadioMouseCursor(), ), ), )); @@ -896,8 +896,8 @@ void main() { }); } -class RadioMouseCursor extends WidgetStateMouseCursor { - const RadioMouseCursor(); +class _RadioMouseCursor extends WidgetStateMouseCursor { + const _RadioMouseCursor(); @override MouseCursor resolve(Set states) { @@ -911,5 +911,5 @@ class RadioMouseCursor extends WidgetStateMouseCursor { } @override - String get debugDescription => 'RadioMouseCursor()'; + String get debugDescription => '_RadioMouseCursor()'; } diff --git a/packages/flutter/test/material/checkbox_test.dart b/packages/flutter/test/material/checkbox_test.dart index 293d7c72ae..cc1fcc1ee6 100644 --- a/packages/flutter/test/material/checkbox_test.dart +++ b/packages/flutter/test/material/checkbox_test.dart @@ -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.iOS, TargetPlatform.macOS})); + testWidgets('Material2 - Checkbox respects fillColor when it is unchecked', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: false); const Color activeBackgroundColor = Color(0xff123456);