Skip to content

Commit d452d04

Browse files
srivats22dkwingsmtpiedciphervictorsanni
authored
#163840 - CupertinoButton cursor doesn't change to clickable on desktop (#164196)
<!-- Thanks for filing a pull request! Reviewers are typically assigned within a week of filing a request. To learn more about code review, see our documentation on Tree Hygiene: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md --> This PR addresses Issue number: 163840, where when hovering over a Cupertino button the mouse cursor wouldn't switch to clickable and there wasn't any option to configure it. Adds Mouse cursor to CupertinoButton, CupertinoButton.Filled and CupertinoButton.Tinted Fixes #163840 Part of #58192 Demo of the changes https://github.com/user-attachments/assets/2e5d874e-cdfe-44bf-9710-bbbde99be3f7 Code snippet showing new behavior ```dart import 'package:flutter/cupertino.dart'; void main() => runApp( // const Center(child: Text('Hello, world!', key: Key('title'), textDirection: TextDirection.ltr)), CupertinoApp( theme: const CupertinoThemeData( brightness: Brightness.light, ), home: Center( child: Column( spacing: 5.0, mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[ CupertinoButton( onPressed: (){}, child: const Text('Default Cursor'), ), CupertinoButton( onPressed: (){}, mouseCursor: SystemMouseCursors.grab, child: const Text('Custom Cursor'), ), CupertinoButton.filled( onPressed: (){}, mouseCursor: SystemMouseCursors.copy, child: const Text('Custom Cursor 2'), ), CupertinoButton.tinted( onPressed: (){}, mouseCursor: SystemMouseCursors.help, child: const Text('Custom Cursor 2'), ), ], ) ), ), ); ``` *List which issues are fixed by this PR. You must list at least one issue. An issue is not required if the PR fixes something trivial like a typo.* *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].* ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --------- Co-authored-by: Tong Mu <dkwingsmt@users.noreply.github.com> Co-authored-by: Tirth <pateltirth454@gmail.com> Co-authored-by: Victor Sanni <victorsanniay@gmail.com>
1 parent dbbfa2f commit d452d04

File tree

2 files changed

+90
-1
lines changed

2 files changed

+90
-1
lines changed

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

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ class CupertinoButton extends StatefulWidget {
9090
this.focusNode,
9191
this.onFocusChange,
9292
this.autofocus = false,
93+
this.mouseCursor,
9394
this.onLongPress,
9495
required this.onPressed,
9596
}) : assert(pressedOpacity == null || (pressedOpacity >= 0.0 && pressedOpacity <= 1.0)),
@@ -125,6 +126,7 @@ class CupertinoButton extends StatefulWidget {
125126
this.focusNode,
126127
this.onFocusChange,
127128
this.autofocus = false,
129+
this.mouseCursor,
128130
this.onLongPress,
129131
required this.onPressed,
130132
}) : assert(minimumSize == null || minSize == null),
@@ -154,6 +156,7 @@ class CupertinoButton extends StatefulWidget {
154156
this.focusNode,
155157
this.onFocusChange,
156158
this.autofocus = false,
159+
this.mouseCursor,
157160
this.onLongPress,
158161
required this.onPressed,
159162
}) : assert(pressedOpacity == null || (pressedOpacity >= 0.0 && pressedOpacity <= 1.0)),
@@ -258,6 +261,23 @@ class CupertinoButton extends StatefulWidget {
258261
/// {@macro flutter.widgets.Focus.autofocus}
259262
final bool autofocus;
260263

264+
/// The cursor for a mouse pointer when it enters or is hovering over the widget.
265+
///
266+
/// If [mouseCursor] is a [WidgetStateMouseCursor],
267+
/// [WidgetStateProperty.resolve] is used for the following [WidgetState]:
268+
/// * [WidgetState.disabled].
269+
///
270+
/// If null, then [MouseCursor.defer] is used when the button is disabled.
271+
/// When the button is enabled, [SystemMouseCursors.click] is used on Web
272+
/// and [MouseCursor.defer] is used on other platforms.
273+
///
274+
/// See also:
275+
///
276+
/// * [WidgetStateMouseCursor], a [MouseCursor] that implements
277+
/// [WidgetStateProperty] which is used in APIs that need to accept
278+
/// either a [MouseCursor] or a [WidgetStateProperty].
279+
final MouseCursor? mouseCursor;
280+
261281
final _CupertinoButtonStyle _style;
262282

263283
/// Whether the button is enabled or disabled. Buttons are disabled by default. To
@@ -297,6 +317,13 @@ class _CupertinoButtonState extends State<CupertinoButton> with SingleTickerProv
297317

298318
late bool isFocused;
299319

320+
static final WidgetStateProperty<MouseCursor> _defaultCursor =
321+
WidgetStateProperty.resolveWith<MouseCursor>((Set<WidgetState> states) {
322+
return !states.contains(WidgetState.disabled) && kIsWeb
323+
? SystemMouseCursors.click
324+
: MouseCursor.defer;
325+
});
326+
300327
@override
301328
void initState() {
302329
super.initState();
@@ -459,9 +486,16 @@ class _CupertinoButtonState extends State<CupertinoButton> with SingleTickerProv
459486
size:
460487
textStyle.fontSize != null ? textStyle.fontSize! * 1.2 : kCupertinoButtonDefaultIconSize,
461488
);
489+
462490
final DeviceGestureSettings? gestureSettings = MediaQuery.maybeGestureSettingsOf(context);
491+
492+
final Set<WidgetState> states = <WidgetState>{if (!enabled) WidgetState.disabled};
493+
final MouseCursor effectiveMouseCursor =
494+
WidgetStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, states) ??
495+
_defaultCursor.resolve(states);
496+
463497
return MouseRegion(
464-
cursor: enabled && kIsWeb ? SystemMouseCursors.click : MouseCursor.defer,
498+
cursor: effectiveMouseCursor,
465499
child: FocusableActionDetector(
466500
actions: _actionMap,
467501
focusNode: widget.focusNode,

packages/flutter/test/cupertino/button_test.dart

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -941,8 +941,63 @@ void main() {
941941
await gesture.up();
942942
expect(value, isTrue);
943943
});
944+
945+
testWidgets('Mouse cursor resolves in enabled/disabled states', (WidgetTester tester) async {
946+
Widget buildButton({required bool enabled, MouseCursor? cursor}) {
947+
return CupertinoApp(
948+
home: Center(
949+
child: CupertinoButton(
950+
onPressed: enabled ? () {} : null,
951+
mouseCursor: cursor,
952+
child: const Text('Tap Me'),
953+
),
954+
),
955+
);
956+
}
957+
958+
// Test default mouse cursor
959+
final TestGesture gesture = await tester.createGesture(
960+
kind: PointerDeviceKind.mouse,
961+
pointer: 1,
962+
);
963+
await tester.pumpWidget(buildButton(enabled: true, cursor: const _ButtonMouseCursor()));
964+
await gesture.addPointer(location: tester.getCenter(find.byType(CupertinoButton)));
965+
await tester.pump();
966+
await gesture.moveTo(tester.getCenter(find.byType(CupertinoButton)));
967+
expect(
968+
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
969+
SystemMouseCursors.basic,
970+
);
971+
await gesture.removePointer();
972+
973+
// Test disabled state mouse cursor
974+
await tester.pumpWidget(buildButton(enabled: false, cursor: const _ButtonMouseCursor()));
975+
await gesture.addPointer(location: tester.getCenter(find.byType(CupertinoButton)));
976+
await tester.pump();
977+
await gesture.moveTo(tester.getCenter(find.byType(CupertinoButton)));
978+
expect(
979+
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
980+
SystemMouseCursors.forbidden,
981+
);
982+
await gesture.removePointer();
983+
});
944984
}
945985

946986
Widget boilerplate({required Widget child}) {
947987
return Directionality(textDirection: TextDirection.ltr, child: Center(child: child));
948988
}
989+
990+
class _ButtonMouseCursor extends WidgetStateMouseCursor {
991+
const _ButtonMouseCursor();
992+
993+
@override
994+
MouseCursor resolve(Set<WidgetState> states) {
995+
if (states.contains(WidgetState.disabled)) {
996+
return SystemMouseCursors.forbidden;
997+
}
998+
return SystemMouseCursors.basic;
999+
}
1000+
1001+
@override
1002+
String get debugDescription => '_ButtonMouseCursor()';
1003+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy