PopupMenu: add themeable mouse cursor v2 (#96567)

This commit is contained in:
Hans Muller 2022-01-14 11:53:17 -08:00 committed by GitHub
parent 5012c99df5
commit 1612310513
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 78 additions and 22 deletions

View file

@ -1087,6 +1087,7 @@ class _InkResponseState extends State<_InkResponseStateWidget>
if (_hasFocus) MaterialState.focused,
},
);
return _ParentInkResponseProvider(
state: this,
child: Actions(

View file

@ -264,15 +264,20 @@ class PopupMenuItem<T> extends PopupMenuEntry<T> {
/// of [ThemeData.textTheme] is used.
final TextStyle? textStyle;
/// {@template flutter.material.popupmenu.mouseCursor}
/// The cursor for a mouse pointer when it enters or is hovering over the
/// widget.
///
/// If [mouseCursor] is a [MaterialStateProperty<MouseCursor>],
/// [MaterialStateProperty.resolve] is used for the following [MaterialState]:
/// [MaterialStateProperty.resolve] is used for the following [MaterialState]s:
///
/// * [MaterialState.hovered].
/// * [MaterialState.focused].
/// * [MaterialState.disabled].
/// {@endtemplate}
///
/// If this property is null, [MaterialStateMouseCursor.clickable] will be used.
/// If null, then the value of [PopupMenuThemeData.mouseCursor] is used. If
/// that is also null, then [MaterialStateMouseCursor.clickable] is used.
final MouseCursor? mouseCursor;
/// The widget below this widget in the tree.
@ -355,12 +360,6 @@ class PopupMenuItemState<T, W extends PopupMenuItem<T>> extends State<W> {
child: item,
);
}
final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor>(
widget.mouseCursor ?? MaterialStateMouseCursor.clickable,
<MaterialState>{
if (!widget.enabled) MaterialState.disabled,
},
);
return MergeSemantics(
child: Semantics(
@ -369,7 +368,7 @@ class PopupMenuItemState<T, W extends PopupMenuItem<T>> extends State<W> {
child: InkWell(
onTap: widget.enabled ? handleTap : null,
canRequestFocus: widget.enabled,
mouseCursor: effectiveMouseCursor,
mouseCursor: _EffectiveMouseCursor(widget.mouseCursor, popupMenuTheme.mouseCursor),
child: item,
),
),
@ -1185,3 +1184,23 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
);
}
}
// This MaterialStateProperty is passed along to the menu item's InkWell which
// resolves the property against MaterialState.disabled, MaterialState.hovered,
// MaterialState.focused.
class _EffectiveMouseCursor extends MaterialStateMouseCursor {
const _EffectiveMouseCursor(this.widgetCursor, this.themeCursor);
final MouseCursor? widgetCursor;
final MaterialStateProperty<MouseCursor?>? themeCursor;
@override
MouseCursor resolve(Set<MaterialState> states) {
return MaterialStateProperty.resolveAs<MouseCursor?>(widgetCursor, states)
?? themeCursor?.resolve(states)
?? MaterialStateMouseCursor.clickable.resolve(states);
}
@override
String get debugDescription => 'MaterialStateMouseCursor(PopupMenuItemState)';
}

View file

@ -7,6 +7,7 @@ import 'dart:ui' show lerpDouble;
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'material_state.dart';
import 'theme.dart';
/// Defines the visual properties of the routes used to display popup menus
@ -38,6 +39,7 @@ class PopupMenuThemeData with Diagnosticable {
this.elevation,
this.textStyle,
this.enableFeedback,
this.mouseCursor,
});
/// The background color of the popup menu.
@ -57,6 +59,11 @@ class PopupMenuThemeData with Diagnosticable {
/// If [PopupMenuButton.enableFeedback] is provided, [enableFeedback] is ignored.
final bool? enableFeedback;
/// {@macro flutter.material.popupmenu.mouseCursor}
///
/// If specified, overrides the default value of [PopupMenuItem.mouseCursor].
final MaterialStateProperty<MouseCursor?>? mouseCursor;
/// Creates a copy of this object with the given fields replaced with the
/// new values.
PopupMenuThemeData copyWith({
@ -65,6 +72,7 @@ class PopupMenuThemeData with Diagnosticable {
double? elevation,
TextStyle? textStyle,
bool? enableFeedback,
MaterialStateProperty<MouseCursor?>? mouseCursor,
}) {
return PopupMenuThemeData(
color: color ?? this.color,
@ -72,6 +80,7 @@ class PopupMenuThemeData with Diagnosticable {
elevation: elevation ?? this.elevation,
textStyle: textStyle ?? this.textStyle,
enableFeedback: enableFeedback ?? this.enableFeedback,
mouseCursor: mouseCursor ?? this.mouseCursor,
);
}
@ -90,6 +99,7 @@ class PopupMenuThemeData with Diagnosticable {
elevation: lerpDouble(a?.elevation, b?.elevation, t),
textStyle: TextStyle.lerp(a?.textStyle, b?.textStyle, t),
enableFeedback: t < 0.5 ? a?.enableFeedback : b?.enableFeedback,
mouseCursor: t < 0.5 ? a?.mouseCursor : b?.mouseCursor,
);
}
@ -101,6 +111,7 @@ class PopupMenuThemeData with Diagnosticable {
elevation,
textStyle,
enableFeedback,
mouseCursor
);
}
@ -115,7 +126,8 @@ class PopupMenuThemeData with Diagnosticable {
&& other.color == color
&& other.shape == shape
&& other.textStyle == textStyle
&& other.enableFeedback == enableFeedback;
&& other.enableFeedback == enableFeedback
&& other.mouseCursor == mouseCursor;
}
@override
@ -126,6 +138,7 @@ class PopupMenuThemeData with Diagnosticable {
properties.add(DoubleProperty('elevation', elevation, defaultValue: null));
properties.add(DiagnosticsProperty<TextStyle>('text style', textStyle, defaultValue: null));
properties.add(DiagnosticsProperty<bool>('enableFeedback', enableFeedback, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<MouseCursor?>>('mouseCursor', mouseCursor, defaultValue: null));
}
}

View file

@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
@ -27,6 +28,7 @@ void main() {
expect(popupMenuTheme.shape, null);
expect(popupMenuTheme.elevation, null);
expect(popupMenuTheme.textStyle, null);
expect(popupMenuTheme.mouseCursor, null);
});
testWidgets('Default PopupMenuThemeData debugFillProperties', (WidgetTester tester) async {
@ -48,6 +50,7 @@ void main() {
shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(2.0))),
elevation: 2.0,
textStyle: TextStyle(color: Color(0xffffffff)),
mouseCursor: MaterialStateMouseCursor.clickable,
).debugFillProperties(builder);
final List<String> description = builder.properties
@ -60,6 +63,7 @@ void main() {
'shape: RoundedRectangleBorder(BorderSide(Color(0xff000000), 0.0, BorderStyle.none), BorderRadius.circular(2.0))',
'elevation: 2.0',
'text style: TextStyle(inherit: true, color: Color(0xffffffff))',
'mouseCursor: MaterialStateMouseCursor(clickable)',
]);
});
@ -251,7 +255,8 @@ void main() {
testWidgets('ThemeData.popupMenuTheme properties are utilized', (WidgetTester tester) async {
final Key popupButtonKey = UniqueKey();
final Key popupButtonApp = UniqueKey();
final Key popupItemKey = UniqueKey();
final Key enabledPopupItemKey = UniqueKey();
final Key disabledPopupItemKey = UniqueKey();
await tester.pumpWidget(MaterialApp(
key: popupButtonApp,
@ -259,19 +264,31 @@ void main() {
child: Column(
children: <Widget>[
PopupMenuTheme(
data: const PopupMenuThemeData(
data: PopupMenuThemeData(
color: Colors.pink,
shape: BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))),
shape: const BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))),
elevation: 6.0,
textStyle: TextStyle(color: Color(0xfffff000), textBaseline: TextBaseline.alphabetic),
textStyle: const TextStyle(color: Color(0xfffff000), textBaseline: TextBaseline.alphabetic),
mouseCursor: MaterialStateProperty.resolveWith<MouseCursor?>((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return SystemMouseCursors.contextMenu;
}
return SystemMouseCursors.alias;
}),
),
child: PopupMenuButton<void>(
key: popupButtonKey,
itemBuilder: (BuildContext context) {
return <PopupMenuEntry<void>>[
PopupMenuItem<void>(
key: popupItemKey,
child: const Text('Example'),
key: disabledPopupItemKey,
enabled: false,
child: const Text('disabled'),
),
PopupMenuItem<void>(
key: enabledPopupItemKey,
onTap: () { },
child: const Text('enabled'),
),
];
},
@ -299,16 +316,22 @@ void main() {
expect(button.shape, const BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))));
expect(button.elevation, 6.0);
/// The last DefaultTextStyle widget under popupItemKey is the
/// [PopupMenuItem] specified above, so by finding the last descendent of
/// popupItemKey that is of type DefaultTextStyle, this code retrieves the
/// built [PopupMenuItem].
final DefaultTextStyle text = tester.widget<DefaultTextStyle>(
find.descendant(
of: find.byKey(popupItemKey),
of: find.byKey(enabledPopupItemKey),
matching: find.byType(DefaultTextStyle),
).last,
),
);
expect(text.style.color, const Color(0xfffff000));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
addTearDown(gesture.removePointer);
await gesture.moveTo(tester.getCenter(find.byKey(disabledPopupItemKey)));
await tester.pumpAndSettle();
expect(RendererBinding.instance!.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.contextMenu);
await gesture.down(tester.getCenter(find.byKey(enabledPopupItemKey)));
await tester.pumpAndSettle();
expect(RendererBinding.instance!.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.alias);
});
}