BottomNavigationBar: add themeable mouse cursor (#96736)

diff --git a/packages/flutter/lib/src/material/bottom_navigation_bar.dart b/packages/flutter/lib/src/material/bottom_navigation_bar.dart
index c40ccbb..59a22c2 100644
--- a/packages/flutter/lib/src/material/bottom_navigation_bar.dart
+++ b/packages/flutter/lib/src/material/bottom_navigation_bar.dart
@@ -14,6 +14,7 @@
 import 'ink_well.dart';
 import 'material.dart';
 import 'material_localizations.dart';
+import 'material_state.dart';
 import 'theme.dart';
 import 'tooltip.dart';
 
@@ -311,9 +312,20 @@
   final bool? showSelectedLabels;
 
   /// The cursor for a mouse pointer when it enters or is hovering over the
-  /// tiles.
+  /// items.
   ///
-  /// If this property is null, [SystemMouseCursors.click] will be used.
+  /// If [mouseCursor] is a [MaterialStateProperty<MouseCursor>],
+  /// [MaterialStateProperty.resolve] is used for the following [MaterialState]s:
+  ///
+  ///  * [MaterialState.selected].
+  ///
+  /// If null, then the value of [BottomNavigationBarThemeData.mouseCursor] is used. If
+  /// that is also null, then [MaterialStateMouseCursor.clickable] is used.
+  ///
+  /// See also:
+  ///
+  ///  * [MaterialStateMouseCursor], which can be used to create a [MouseCursor]
+  ///    that is also a [MaterialStateProperty<MouseCursor>].
   final MouseCursor? mouseCursor;
 
   /// Whether detected gestures should provide acoustic and/or haptic feedback.
@@ -941,10 +953,17 @@
         );
         break;
     }
-    final MouseCursor effectiveMouseCursor = widget.mouseCursor ?? SystemMouseCursors.click;
 
     final List<Widget> tiles = <Widget>[];
     for (int i = 0; i < widget.items.length; i++) {
+      final Set<MaterialState> states = <MaterialState>{
+        if (i == widget.currentIndex) MaterialState.selected,
+      };
+
+      final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, states)
+        ?? bottomTheme.mouseCursor?.resolve(states)
+        ?? MaterialStateMouseCursor.clickable.resolve(states);
+
       tiles.add(_BottomNavigationTile(
         _effectiveType,
         widget.items[i],
diff --git a/packages/flutter/lib/src/material/bottom_navigation_bar_theme.dart b/packages/flutter/lib/src/material/bottom_navigation_bar_theme.dart
index df0dd80..101c9ee 100644
--- a/packages/flutter/lib/src/material/bottom_navigation_bar_theme.dart
+++ b/packages/flutter/lib/src/material/bottom_navigation_bar_theme.dart
@@ -8,6 +8,7 @@
 import 'package:flutter/widgets.dart';
 
 import 'bottom_navigation_bar.dart';
+import 'material_state.dart';
 import 'theme.dart';
 
 /// Defines default property values for descendant [BottomNavigationBar]
@@ -45,6 +46,7 @@
     this.type,
     this.enableFeedback,
     this.landscapeLayout,
+    this.mouseCursor,
   });
 
   /// The color of the [BottomNavigationBar] itself.
@@ -124,6 +126,9 @@
   /// If non-null, overrides the [BottomNavigationBar.landscapeLayout] property.
   final BottomNavigationBarLandscapeLayout? landscapeLayout;
 
+  /// If specified, overrides the default value of [BottomNavigationBar.mouseCursor].
+  final MaterialStateProperty<MouseCursor?>? mouseCursor;
+
   /// Creates a copy of this object but with the given fields replaced with the
   /// new values.
   BottomNavigationBarThemeData copyWith({
@@ -139,7 +144,8 @@
     bool? showUnselectedLabels,
     BottomNavigationBarType? type,
     bool? enableFeedback,
-    BottomNavigationBarLandscapeLayout? landscapeLayout
+    BottomNavigationBarLandscapeLayout? landscapeLayout,
+    MaterialStateProperty<MouseCursor?>? mouseCursor,
   }) {
     return BottomNavigationBarThemeData(
       backgroundColor: backgroundColor ?? this.backgroundColor,
@@ -155,6 +161,7 @@
       type: type ?? this.type,
       enableFeedback: enableFeedback ?? this.enableFeedback,
       landscapeLayout: landscapeLayout ?? this.landscapeLayout,
+      mouseCursor: mouseCursor ?? this.mouseCursor,
     );
   }
 
@@ -179,6 +186,7 @@
       type: t < 0.5 ? a?.type : b?.type,
       enableFeedback: t < 0.5 ? a?.enableFeedback : b?.enableFeedback,
       landscapeLayout: t < 0.5 ? a?.landscapeLayout : b?.landscapeLayout,
+      mouseCursor: t < 0.5 ? a?.mouseCursor : b?.mouseCursor,
     );
   }
 
@@ -198,6 +206,7 @@
       type,
       enableFeedback,
       landscapeLayout,
+      mouseCursor,
     );
   }
 
@@ -220,7 +229,8 @@
         && other.showUnselectedLabels == showUnselectedLabels
         && other.type == type
         && other.enableFeedback == enableFeedback
-        && other.landscapeLayout == landscapeLayout;
+        && other.landscapeLayout == landscapeLayout
+        && other.mouseCursor == mouseCursor;
   }
 
   @override
@@ -239,6 +249,7 @@
     properties.add(DiagnosticsProperty<BottomNavigationBarType>('type', type, defaultValue: null));
     properties.add(DiagnosticsProperty<bool>('enableFeedback', enableFeedback, defaultValue: null));
     properties.add(DiagnosticsProperty<BottomNavigationBarLandscapeLayout>('landscapeLayout', landscapeLayout, defaultValue: null));
+    properties.add(DiagnosticsProperty<MaterialStateProperty<MouseCursor?>>('mouseCursor', mouseCursor, defaultValue: null));
   }
 }
 
diff --git a/packages/flutter/test/material/bottom_navigation_bar_theme_test.dart b/packages/flutter/test/material/bottom_navigation_bar_theme_test.dart
index f2b2703..1ade4bd 100644
--- a/packages/flutter/test/material/bottom_navigation_bar_theme_test.dart
+++ b/packages/flutter/test/material/bottom_navigation_bar_theme_test.dart
@@ -2,8 +2,10 @@
 // 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/services.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:vector_math/vector_math_64.dart' show Vector3;
 
@@ -28,6 +30,7 @@
     expect(themeData.showUnselectedLabels, null);
     expect(themeData.type, null);
     expect(themeData.landscapeLayout, null);
+    expect(themeData.mouseCursor, null);
 
     const BottomNavigationBarTheme theme = BottomNavigationBarTheme(data: BottomNavigationBarThemeData(), child: SizedBox());
     expect(theme.data.backgroundColor, null);
@@ -42,6 +45,7 @@
     expect(theme.data.showUnselectedLabels, null);
     expect(theme.data.type, null);
     expect(themeData.landscapeLayout, null);
+    expect(themeData.mouseCursor, null);
   });
 
   testWidgets('Default BottomNavigationBarThemeData debugFillProperties', (WidgetTester tester) async {
@@ -70,6 +74,7 @@
       showSelectedLabels: true,
       showUnselectedLabels: true,
       type: BottomNavigationBarType.fixed,
+      mouseCursor: MaterialStateMouseCursor.clickable,
     ).debugFillProperties(builder);
 
     final List<String> description = builder.properties
@@ -93,6 +98,7 @@
     expect(description[8], 'showSelectedLabels: true');
     expect(description[9], 'showUnselectedLabels: true');
     expect(description[10], 'type: BottomNavigationBarType.fixed');
+    expect(description[11], 'mouseCursor: MaterialStateMouseCursor(clickable)');
   });
 
   testWidgets('BottomNavigationBar is themeable', (WidgetTester tester) async {
@@ -108,7 +114,7 @@
     await tester.pumpWidget(
       MaterialApp(
         theme: ThemeData(
-          bottomNavigationBarTheme: const BottomNavigationBarThemeData(
+          bottomNavigationBarTheme: BottomNavigationBarThemeData(
             backgroundColor: backgroundColor,
             selectedItemColor: selectedItemColor,
             unselectedItemColor: unselectedItemColor,
@@ -120,6 +126,12 @@
             type: BottomNavigationBarType.fixed,
             selectedLabelStyle: selectedTextStyle,
             unselectedLabelStyle: unselectedTextStyle,
+            mouseCursor: MaterialStateProperty.resolveWith<MouseCursor?>((Set<MaterialState> states) {
+              if (states.contains(MaterialState.selected)) {
+                return SystemMouseCursors.grab;
+              }
+              return SystemMouseCursors.move;
+            }),
           ),
         ),
         home: Scaffold(
@@ -166,6 +178,24 @@
     expect(findFadeTransition, findsNothing);
     expect(_material(tester).elevation, equals(elevation));
     expect(_material(tester).color, equals(backgroundColor));
+
+    final Offset selectedBarItem = tester.getCenter(
+      find.ancestor(of: find.text('AC'),
+      matching: find.byType(Transform))
+    );
+    final Offset unselectedBarItem = tester.getCenter(
+      find.ancestor(of: find.text('Alarm'),
+      matching: find.byType(Transform))
+    );
+    final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
+    await gesture.addPointer();
+    addTearDown(gesture.removePointer);
+    await gesture.moveTo(selectedBarItem);
+    await tester.pumpAndSettle();
+    expect(RendererBinding.instance!.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.grab);
+    await gesture.moveTo(unselectedBarItem);
+    await tester.pumpAndSettle();
+    expect(RendererBinding.instance!.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.move);
   });
 
   testWidgets('BottomNavigationBar properties are taken over the theme values', (WidgetTester tester) async {
@@ -178,6 +208,7 @@
     const TextStyle themeUnselectedTextStyle = TextStyle(fontSize: 21);
     const double themeElevation = 9.0;
     const BottomNavigationBarLandscapeLayout themeLandscapeLayout = BottomNavigationBarLandscapeLayout.centered;
+    const MaterialStateMouseCursor themeCursor = MaterialStateMouseCursor.clickable;
 
     const Color backgroundColor = Color(0xFF000004);
     const Color selectedItemColor = Color(0xFF000005);
@@ -188,6 +219,7 @@
     const TextStyle unselectedTextStyle = TextStyle(fontSize: 26);
     const double elevation = 7.0;
     const BottomNavigationBarLandscapeLayout landscapeLayout = BottomNavigationBarLandscapeLayout.spread;
+    const MaterialStateMouseCursor cursor = MaterialStateMouseCursor.textable;
 
     await tester.pumpWidget(
       MaterialApp(
@@ -205,6 +237,7 @@
             selectedLabelStyle: themeSelectedTextStyle,
             unselectedLabelStyle: themeUnselectedTextStyle,
             landscapeLayout: themeLandscapeLayout,
+            mouseCursor: themeCursor,
           ),
         ),
         home: Scaffold(
@@ -221,6 +254,7 @@
             selectedLabelStyle: selectedTextStyle,
             unselectedLabelStyle: unselectedTextStyle,
             landscapeLayout: landscapeLayout,
+            mouseCursor: cursor,
             items: const <BottomNavigationBarItem>[
               BottomNavigationBarItem(
                 icon: Icon(Icons.ac_unit),
@@ -263,6 +297,17 @@
     expect(findFadeTransition, findsNothing);
     expect(_material(tester).elevation, equals(elevation));
     expect(_material(tester).color, equals(backgroundColor));
+
+    final Offset barItem = tester.getCenter(
+      find.ancestor(of: find.text('AC'),
+      matching: find.byType(Transform))
+    );
+    final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
+    await gesture.addPointer();
+    addTearDown(gesture.removePointer);
+    await gesture.moveTo(barItem);
+    await tester.pumpAndSettle();
+    expect(RendererBinding.instance!.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
   });
 
   testWidgets('BottomNavigationBarTheme can be used to hide all labels', (WidgetTester tester) async {