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);
   });
 }