<!-- 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 https://github.com/flutter/flutter/issues/163840 Part of https://github.com/flutter/flutter/issues/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>
This commit is contained in:
parent
dbbfa2ff31
commit
d452d04a07
@ -90,6 +90,7 @@ class CupertinoButton extends StatefulWidget {
|
|||||||
this.focusNode,
|
this.focusNode,
|
||||||
this.onFocusChange,
|
this.onFocusChange,
|
||||||
this.autofocus = false,
|
this.autofocus = false,
|
||||||
|
this.mouseCursor,
|
||||||
this.onLongPress,
|
this.onLongPress,
|
||||||
required this.onPressed,
|
required this.onPressed,
|
||||||
}) : assert(pressedOpacity == null || (pressedOpacity >= 0.0 && pressedOpacity <= 1.0)),
|
}) : assert(pressedOpacity == null || (pressedOpacity >= 0.0 && pressedOpacity <= 1.0)),
|
||||||
@ -125,6 +126,7 @@ class CupertinoButton extends StatefulWidget {
|
|||||||
this.focusNode,
|
this.focusNode,
|
||||||
this.onFocusChange,
|
this.onFocusChange,
|
||||||
this.autofocus = false,
|
this.autofocus = false,
|
||||||
|
this.mouseCursor,
|
||||||
this.onLongPress,
|
this.onLongPress,
|
||||||
required this.onPressed,
|
required this.onPressed,
|
||||||
}) : assert(minimumSize == null || minSize == null),
|
}) : assert(minimumSize == null || minSize == null),
|
||||||
@ -154,6 +156,7 @@ class CupertinoButton extends StatefulWidget {
|
|||||||
this.focusNode,
|
this.focusNode,
|
||||||
this.onFocusChange,
|
this.onFocusChange,
|
||||||
this.autofocus = false,
|
this.autofocus = false,
|
||||||
|
this.mouseCursor,
|
||||||
this.onLongPress,
|
this.onLongPress,
|
||||||
required this.onPressed,
|
required this.onPressed,
|
||||||
}) : assert(pressedOpacity == null || (pressedOpacity >= 0.0 && pressedOpacity <= 1.0)),
|
}) : assert(pressedOpacity == null || (pressedOpacity >= 0.0 && pressedOpacity <= 1.0)),
|
||||||
@ -258,6 +261,23 @@ class CupertinoButton extends StatefulWidget {
|
|||||||
/// {@macro flutter.widgets.Focus.autofocus}
|
/// {@macro flutter.widgets.Focus.autofocus}
|
||||||
final bool autofocus;
|
final bool autofocus;
|
||||||
|
|
||||||
|
/// 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]:
|
||||||
|
/// * [WidgetState.disabled].
|
||||||
|
///
|
||||||
|
/// If null, then [MouseCursor.defer] is used when the button is disabled.
|
||||||
|
/// When the button is enabled, [SystemMouseCursors.click] is used on Web
|
||||||
|
/// and [MouseCursor.defer] 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;
|
||||||
|
|
||||||
final _CupertinoButtonStyle _style;
|
final _CupertinoButtonStyle _style;
|
||||||
|
|
||||||
/// Whether the button is enabled or disabled. Buttons are disabled by default. To
|
/// Whether the button is enabled or disabled. Buttons are disabled by default. To
|
||||||
@ -297,6 +317,13 @@ class _CupertinoButtonState extends State<CupertinoButton> with SingleTickerProv
|
|||||||
|
|
||||||
late bool isFocused;
|
late bool isFocused;
|
||||||
|
|
||||||
|
static final WidgetStateProperty<MouseCursor> _defaultCursor =
|
||||||
|
WidgetStateProperty.resolveWith<MouseCursor>((Set<WidgetState> states) {
|
||||||
|
return !states.contains(WidgetState.disabled) && kIsWeb
|
||||||
|
? SystemMouseCursors.click
|
||||||
|
: MouseCursor.defer;
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -459,9 +486,16 @@ class _CupertinoButtonState extends State<CupertinoButton> with SingleTickerProv
|
|||||||
size:
|
size:
|
||||||
textStyle.fontSize != null ? textStyle.fontSize! * 1.2 : kCupertinoButtonDefaultIconSize,
|
textStyle.fontSize != null ? textStyle.fontSize! * 1.2 : kCupertinoButtonDefaultIconSize,
|
||||||
);
|
);
|
||||||
|
|
||||||
final DeviceGestureSettings? gestureSettings = MediaQuery.maybeGestureSettingsOf(context);
|
final DeviceGestureSettings? gestureSettings = MediaQuery.maybeGestureSettingsOf(context);
|
||||||
|
|
||||||
|
final Set<WidgetState> states = <WidgetState>{if (!enabled) WidgetState.disabled};
|
||||||
|
final MouseCursor effectiveMouseCursor =
|
||||||
|
WidgetStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, states) ??
|
||||||
|
_defaultCursor.resolve(states);
|
||||||
|
|
||||||
return MouseRegion(
|
return MouseRegion(
|
||||||
cursor: enabled && kIsWeb ? SystemMouseCursors.click : MouseCursor.defer,
|
cursor: effectiveMouseCursor,
|
||||||
child: FocusableActionDetector(
|
child: FocusableActionDetector(
|
||||||
actions: _actionMap,
|
actions: _actionMap,
|
||||||
focusNode: widget.focusNode,
|
focusNode: widget.focusNode,
|
||||||
|
@ -941,8 +941,63 @@ void main() {
|
|||||||
await gesture.up();
|
await gesture.up();
|
||||||
expect(value, isTrue);
|
expect(value, isTrue);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('Mouse cursor resolves in enabled/disabled states', (WidgetTester tester) async {
|
||||||
|
Widget buildButton({required bool enabled, MouseCursor? cursor}) {
|
||||||
|
return CupertinoApp(
|
||||||
|
home: Center(
|
||||||
|
child: CupertinoButton(
|
||||||
|
onPressed: enabled ? () {} : null,
|
||||||
|
mouseCursor: cursor,
|
||||||
|
child: const Text('Tap Me'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test default mouse cursor
|
||||||
|
final TestGesture gesture = await tester.createGesture(
|
||||||
|
kind: PointerDeviceKind.mouse,
|
||||||
|
pointer: 1,
|
||||||
|
);
|
||||||
|
await tester.pumpWidget(buildButton(enabled: true, cursor: const _ButtonMouseCursor()));
|
||||||
|
await gesture.addPointer(location: tester.getCenter(find.byType(CupertinoButton)));
|
||||||
|
await tester.pump();
|
||||||
|
await gesture.moveTo(tester.getCenter(find.byType(CupertinoButton)));
|
||||||
|
expect(
|
||||||
|
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
|
||||||
|
SystemMouseCursors.basic,
|
||||||
|
);
|
||||||
|
await gesture.removePointer();
|
||||||
|
|
||||||
|
// Test disabled state mouse cursor
|
||||||
|
await tester.pumpWidget(buildButton(enabled: false, cursor: const _ButtonMouseCursor()));
|
||||||
|
await gesture.addPointer(location: tester.getCenter(find.byType(CupertinoButton)));
|
||||||
|
await tester.pump();
|
||||||
|
await gesture.moveTo(tester.getCenter(find.byType(CupertinoButton)));
|
||||||
|
expect(
|
||||||
|
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
|
||||||
|
SystemMouseCursors.forbidden,
|
||||||
|
);
|
||||||
|
await gesture.removePointer();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget boilerplate({required Widget child}) {
|
Widget boilerplate({required Widget child}) {
|
||||||
return Directionality(textDirection: TextDirection.ltr, child: Center(child: child));
|
return Directionality(textDirection: TextDirection.ltr, child: Center(child: child));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _ButtonMouseCursor extends WidgetStateMouseCursor {
|
||||||
|
const _ButtonMouseCursor();
|
||||||
|
|
||||||
|
@override
|
||||||
|
MouseCursor resolve(Set<WidgetState> states) {
|
||||||
|
if (states.contains(WidgetState.disabled)) {
|
||||||
|
return SystemMouseCursors.forbidden;
|
||||||
|
}
|
||||||
|
return SystemMouseCursors.basic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get debugDescription => '_ButtonMouseCursor()';
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user