PopupMenu: add themeable mouse cursor v2 (#96567)
diff --git a/packages/flutter/lib/src/material/ink_well.dart b/packages/flutter/lib/src/material/ink_well.dart
index dbac66f..2529ca5 100644
--- a/packages/flutter/lib/src/material/ink_well.dart
+++ b/packages/flutter/lib/src/material/ink_well.dart
@@ -1087,6 +1087,7 @@
if (_hasFocus) MaterialState.focused,
},
);
+
return _ParentInkResponseProvider(
state: this,
child: Actions(
diff --git a/packages/flutter/lib/src/material/popup_menu.dart b/packages/flutter/lib/src/material/popup_menu.dart
index ca6f64c..a019c96 100644
--- a/packages/flutter/lib/src/material/popup_menu.dart
+++ b/packages/flutter/lib/src/material/popup_menu.dart
@@ -264,15 +264,20 @@
/// 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 @@
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 @@
child: InkWell(
onTap: widget.enabled ? handleTap : null,
canRequestFocus: widget.enabled,
- mouseCursor: effectiveMouseCursor,
+ mouseCursor: _EffectiveMouseCursor(widget.mouseCursor, popupMenuTheme.mouseCursor),
child: item,
),
),
@@ -1185,3 +1184,23 @@
);
}
}
+
+// 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)';
+}
diff --git a/packages/flutter/lib/src/material/popup_menu_theme.dart b/packages/flutter/lib/src/material/popup_menu_theme.dart
index a05ddd8..2a899f8 100644
--- a/packages/flutter/lib/src/material/popup_menu_theme.dart
+++ b/packages/flutter/lib/src/material/popup_menu_theme.dart
@@ -7,6 +7,7 @@
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 @@
this.elevation,
this.textStyle,
this.enableFeedback,
+ this.mouseCursor,
});
/// The background color of the popup menu.
@@ -57,6 +59,11 @@
/// 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 @@
double? elevation,
TextStyle? textStyle,
bool? enableFeedback,
+ MaterialStateProperty<MouseCursor?>? mouseCursor,
}) {
return PopupMenuThemeData(
color: color ?? this.color,
@@ -72,6 +80,7 @@
elevation: elevation ?? this.elevation,
textStyle: textStyle ?? this.textStyle,
enableFeedback: enableFeedback ?? this.enableFeedback,
+ mouseCursor: mouseCursor ?? this.mouseCursor,
);
}
@@ -90,6 +99,7 @@
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 @@
elevation,
textStyle,
enableFeedback,
+ mouseCursor
);
}
@@ -115,7 +126,8 @@
&& other.color == color
&& other.shape == shape
&& other.textStyle == textStyle
- && other.enableFeedback == enableFeedback;
+ && other.enableFeedback == enableFeedback
+ && other.mouseCursor == mouseCursor;
}
@override
@@ -126,6 +138,7 @@
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));
}
}
diff --git a/packages/flutter/test/material/popup_menu_theme_test.dart b/packages/flutter/test/material/popup_menu_theme_test.dart
index 444dabf..35a1f45 100644
--- a/packages/flutter/test/material/popup_menu_theme_test.dart
+++ b/packages/flutter/test/material/popup_menu_theme_test.dart
@@ -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 @@
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 @@
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 @@
'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 @@
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 @@
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 @@
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);
});
}