Unverified Commit 16123105 authored by Hans Muller's avatar Hans Muller Committed by GitHub

PopupMenu: add themeable mouse cursor v2 (#96567)

parent 5012c99d
...@@ -1087,6 +1087,7 @@ class _InkResponseState extends State<_InkResponseStateWidget> ...@@ -1087,6 +1087,7 @@ class _InkResponseState extends State<_InkResponseStateWidget>
if (_hasFocus) MaterialState.focused, if (_hasFocus) MaterialState.focused,
}, },
); );
return _ParentInkResponseProvider( return _ParentInkResponseProvider(
state: this, state: this,
child: Actions( child: Actions(
......
...@@ -264,15 +264,20 @@ class PopupMenuItem<T> extends PopupMenuEntry<T> { ...@@ -264,15 +264,20 @@ class PopupMenuItem<T> extends PopupMenuEntry<T> {
/// of [ThemeData.textTheme] is used. /// of [ThemeData.textTheme] is used.
final TextStyle? textStyle; final TextStyle? textStyle;
/// {@template flutter.material.popupmenu.mouseCursor}
/// 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 [MaterialStateProperty<MouseCursor>], /// 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]. /// * [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; final MouseCursor? mouseCursor;
/// The widget below this widget in the tree. /// The widget below this widget in the tree.
...@@ -355,12 +360,6 @@ class PopupMenuItemState<T, W extends PopupMenuItem<T>> extends State<W> { ...@@ -355,12 +360,6 @@ class PopupMenuItemState<T, W extends PopupMenuItem<T>> extends State<W> {
child: item, child: item,
); );
} }
final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor>(
widget.mouseCursor ?? MaterialStateMouseCursor.clickable,
<MaterialState>{
if (!widget.enabled) MaterialState.disabled,
},
);
return MergeSemantics( return MergeSemantics(
child: Semantics( child: Semantics(
...@@ -369,7 +368,7 @@ class PopupMenuItemState<T, W extends PopupMenuItem<T>> extends State<W> { ...@@ -369,7 +368,7 @@ class PopupMenuItemState<T, W extends PopupMenuItem<T>> extends State<W> {
child: InkWell( child: InkWell(
onTap: widget.enabled ? handleTap : null, onTap: widget.enabled ? handleTap : null,
canRequestFocus: widget.enabled, canRequestFocus: widget.enabled,
mouseCursor: effectiveMouseCursor, mouseCursor: _EffectiveMouseCursor(widget.mouseCursor, popupMenuTheme.mouseCursor),
child: item, child: item,
), ),
), ),
...@@ -1185,3 +1184,23 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> { ...@@ -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)';
}
...@@ -7,6 +7,7 @@ import 'dart:ui' show lerpDouble; ...@@ -7,6 +7,7 @@ import 'dart:ui' show lerpDouble;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'material_state.dart';
import 'theme.dart'; import 'theme.dart';
/// Defines the visual properties of the routes used to display popup menus /// Defines the visual properties of the routes used to display popup menus
...@@ -38,6 +39,7 @@ class PopupMenuThemeData with Diagnosticable { ...@@ -38,6 +39,7 @@ class PopupMenuThemeData with Diagnosticable {
this.elevation, this.elevation,
this.textStyle, this.textStyle,
this.enableFeedback, this.enableFeedback,
this.mouseCursor,
}); });
/// The background color of the popup menu. /// The background color of the popup menu.
...@@ -57,6 +59,11 @@ class PopupMenuThemeData with Diagnosticable { ...@@ -57,6 +59,11 @@ class PopupMenuThemeData with Diagnosticable {
/// If [PopupMenuButton.enableFeedback] is provided, [enableFeedback] is ignored. /// If [PopupMenuButton.enableFeedback] is provided, [enableFeedback] is ignored.
final bool? enableFeedback; 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 /// Creates a copy of this object with the given fields replaced with the
/// new values. /// new values.
PopupMenuThemeData copyWith({ PopupMenuThemeData copyWith({
...@@ -65,6 +72,7 @@ class PopupMenuThemeData with Diagnosticable { ...@@ -65,6 +72,7 @@ class PopupMenuThemeData with Diagnosticable {
double? elevation, double? elevation,
TextStyle? textStyle, TextStyle? textStyle,
bool? enableFeedback, bool? enableFeedback,
MaterialStateProperty<MouseCursor?>? mouseCursor,
}) { }) {
return PopupMenuThemeData( return PopupMenuThemeData(
color: color ?? this.color, color: color ?? this.color,
...@@ -72,6 +80,7 @@ class PopupMenuThemeData with Diagnosticable { ...@@ -72,6 +80,7 @@ class PopupMenuThemeData with Diagnosticable {
elevation: elevation ?? this.elevation, elevation: elevation ?? this.elevation,
textStyle: textStyle ?? this.textStyle, textStyle: textStyle ?? this.textStyle,
enableFeedback: enableFeedback ?? this.enableFeedback, enableFeedback: enableFeedback ?? this.enableFeedback,
mouseCursor: mouseCursor ?? this.mouseCursor,
); );
} }
...@@ -90,6 +99,7 @@ class PopupMenuThemeData with Diagnosticable { ...@@ -90,6 +99,7 @@ class PopupMenuThemeData with Diagnosticable {
elevation: lerpDouble(a?.elevation, b?.elevation, t), elevation: lerpDouble(a?.elevation, b?.elevation, t),
textStyle: TextStyle.lerp(a?.textStyle, b?.textStyle, t), textStyle: TextStyle.lerp(a?.textStyle, b?.textStyle, t),
enableFeedback: t < 0.5 ? a?.enableFeedback : b?.enableFeedback, enableFeedback: t < 0.5 ? a?.enableFeedback : b?.enableFeedback,
mouseCursor: t < 0.5 ? a?.mouseCursor : b?.mouseCursor,
); );
} }
...@@ -101,6 +111,7 @@ class PopupMenuThemeData with Diagnosticable { ...@@ -101,6 +111,7 @@ class PopupMenuThemeData with Diagnosticable {
elevation, elevation,
textStyle, textStyle,
enableFeedback, enableFeedback,
mouseCursor
); );
} }
...@@ -115,7 +126,8 @@ class PopupMenuThemeData with Diagnosticable { ...@@ -115,7 +126,8 @@ class PopupMenuThemeData with Diagnosticable {
&& other.color == color && other.color == color
&& other.shape == shape && other.shape == shape
&& other.textStyle == textStyle && other.textStyle == textStyle
&& other.enableFeedback == enableFeedback; && other.enableFeedback == enableFeedback
&& other.mouseCursor == mouseCursor;
} }
@override @override
...@@ -126,6 +138,7 @@ class PopupMenuThemeData with Diagnosticable { ...@@ -126,6 +138,7 @@ class PopupMenuThemeData with Diagnosticable {
properties.add(DoubleProperty('elevation', elevation, defaultValue: null)); properties.add(DoubleProperty('elevation', elevation, defaultValue: null));
properties.add(DiagnosticsProperty<TextStyle>('text style', textStyle, defaultValue: null)); properties.add(DiagnosticsProperty<TextStyle>('text style', textStyle, defaultValue: null));
properties.add(DiagnosticsProperty<bool>('enableFeedback', enableFeedback, defaultValue: null)); properties.add(DiagnosticsProperty<bool>('enableFeedback', enableFeedback, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<MouseCursor?>>('mouseCursor', mouseCursor, defaultValue: null));
} }
} }
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -27,6 +28,7 @@ void main() { ...@@ -27,6 +28,7 @@ void main() {
expect(popupMenuTheme.shape, null); expect(popupMenuTheme.shape, null);
expect(popupMenuTheme.elevation, null); expect(popupMenuTheme.elevation, null);
expect(popupMenuTheme.textStyle, null); expect(popupMenuTheme.textStyle, null);
expect(popupMenuTheme.mouseCursor, null);
}); });
testWidgets('Default PopupMenuThemeData debugFillProperties', (WidgetTester tester) async { testWidgets('Default PopupMenuThemeData debugFillProperties', (WidgetTester tester) async {
...@@ -48,6 +50,7 @@ void main() { ...@@ -48,6 +50,7 @@ void main() {
shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(2.0))), shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(2.0))),
elevation: 2.0, elevation: 2.0,
textStyle: TextStyle(color: Color(0xffffffff)), textStyle: TextStyle(color: Color(0xffffffff)),
mouseCursor: MaterialStateMouseCursor.clickable,
).debugFillProperties(builder); ).debugFillProperties(builder);
final List<String> description = builder.properties final List<String> description = builder.properties
...@@ -60,6 +63,7 @@ void main() { ...@@ -60,6 +63,7 @@ void main() {
'shape: RoundedRectangleBorder(BorderSide(Color(0xff000000), 0.0, BorderStyle.none), BorderRadius.circular(2.0))', 'shape: RoundedRectangleBorder(BorderSide(Color(0xff000000), 0.0, BorderStyle.none), BorderRadius.circular(2.0))',
'elevation: 2.0', 'elevation: 2.0',
'text style: TextStyle(inherit: true, color: Color(0xffffffff))', 'text style: TextStyle(inherit: true, color: Color(0xffffffff))',
'mouseCursor: MaterialStateMouseCursor(clickable)',
]); ]);
}); });
...@@ -251,7 +255,8 @@ void main() { ...@@ -251,7 +255,8 @@ void main() {
testWidgets('ThemeData.popupMenuTheme properties are utilized', (WidgetTester tester) async { testWidgets('ThemeData.popupMenuTheme properties are utilized', (WidgetTester tester) async {
final Key popupButtonKey = UniqueKey(); final Key popupButtonKey = UniqueKey();
final Key popupButtonApp = UniqueKey(); final Key popupButtonApp = UniqueKey();
final Key popupItemKey = UniqueKey(); final Key enabledPopupItemKey = UniqueKey();
final Key disabledPopupItemKey = UniqueKey();
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
key: popupButtonApp, key: popupButtonApp,
...@@ -259,19 +264,31 @@ void main() { ...@@ -259,19 +264,31 @@ void main() {
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
PopupMenuTheme( PopupMenuTheme(
data: const PopupMenuThemeData( data: PopupMenuThemeData(
color: Colors.pink, color: Colors.pink,
shape: BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))), shape: const BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))),
elevation: 6.0, 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>( child: PopupMenuButton<void>(
key: popupButtonKey, key: popupButtonKey,
itemBuilder: (BuildContext context) { itemBuilder: (BuildContext context) {
return <PopupMenuEntry<void>>[ return <PopupMenuEntry<void>>[
PopupMenuItem<void>( PopupMenuItem<void>(
key: popupItemKey, key: disabledPopupItemKey,
child: const Text('Example'), enabled: false,
child: const Text('disabled'),
),
PopupMenuItem<void>(
key: enabledPopupItemKey,
onTap: () { },
child: const Text('enabled'),
), ),
]; ];
}, },
...@@ -299,16 +316,22 @@ void main() { ...@@ -299,16 +316,22 @@ void main() {
expect(button.shape, const BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10)))); expect(button.shape, const BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))));
expect(button.elevation, 6.0); 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>( final DefaultTextStyle text = tester.widget<DefaultTextStyle>(
find.descendant( find.descendant(
of: find.byKey(popupItemKey), of: find.byKey(enabledPopupItemKey),
matching: find.byType(DefaultTextStyle), matching: find.byType(DefaultTextStyle),
).last, ),
); );
expect(text.style.color, const Color(0xfffff000)); 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);
}); });
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment