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 {