Unverified Commit 7a4f5ebf authored by Taha Tesser's avatar Taha Tesser Committed by GitHub

BottomNavigationBar: add themeable mouse cursor (#96736)

parent 6ceb418b
...@@ -14,6 +14,7 @@ import 'debug.dart'; ...@@ -14,6 +14,7 @@ import 'debug.dart';
import 'ink_well.dart'; import 'ink_well.dart';
import 'material.dart'; import 'material.dart';
import 'material_localizations.dart'; import 'material_localizations.dart';
import 'material_state.dart';
import 'theme.dart'; import 'theme.dart';
import 'tooltip.dart'; import 'tooltip.dart';
...@@ -311,9 +312,20 @@ class BottomNavigationBar extends StatefulWidget { ...@@ -311,9 +312,20 @@ class BottomNavigationBar extends StatefulWidget {
final bool? showSelectedLabels; final bool? showSelectedLabels;
/// 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
/// 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; final MouseCursor? mouseCursor;
/// Whether detected gestures should provide acoustic and/or haptic feedback. /// Whether detected gestures should provide acoustic and/or haptic feedback.
...@@ -941,10 +953,17 @@ class _BottomNavigationBarState extends State<BottomNavigationBar> with TickerPr ...@@ -941,10 +953,17 @@ class _BottomNavigationBarState extends State<BottomNavigationBar> with TickerPr
); );
break; break;
} }
final MouseCursor effectiveMouseCursor = widget.mouseCursor ?? SystemMouseCursors.click;
final List<Widget> tiles = <Widget>[]; final List<Widget> tiles = <Widget>[];
for (int i = 0; i < widget.items.length; i++) { 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( tiles.add(_BottomNavigationTile(
_effectiveType, _effectiveType,
widget.items[i], widget.items[i],
......
...@@ -8,6 +8,7 @@ import 'package:flutter/foundation.dart'; ...@@ -8,6 +8,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'bottom_navigation_bar.dart'; import 'bottom_navigation_bar.dart';
import 'material_state.dart';
import 'theme.dart'; import 'theme.dart';
/// Defines default property values for descendant [BottomNavigationBar] /// Defines default property values for descendant [BottomNavigationBar]
...@@ -45,6 +46,7 @@ class BottomNavigationBarThemeData with Diagnosticable { ...@@ -45,6 +46,7 @@ class BottomNavigationBarThemeData with Diagnosticable {
this.type, this.type,
this.enableFeedback, this.enableFeedback,
this.landscapeLayout, this.landscapeLayout,
this.mouseCursor,
}); });
/// The color of the [BottomNavigationBar] itself. /// The color of the [BottomNavigationBar] itself.
...@@ -124,6 +126,9 @@ class BottomNavigationBarThemeData with Diagnosticable { ...@@ -124,6 +126,9 @@ class BottomNavigationBarThemeData with Diagnosticable {
/// If non-null, overrides the [BottomNavigationBar.landscapeLayout] property. /// If non-null, overrides the [BottomNavigationBar.landscapeLayout] property.
final BottomNavigationBarLandscapeLayout? landscapeLayout; 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 /// Creates a copy of this object but with the given fields replaced with the
/// new values. /// new values.
BottomNavigationBarThemeData copyWith({ BottomNavigationBarThemeData copyWith({
...@@ -139,7 +144,8 @@ class BottomNavigationBarThemeData with Diagnosticable { ...@@ -139,7 +144,8 @@ class BottomNavigationBarThemeData with Diagnosticable {
bool? showUnselectedLabels, bool? showUnselectedLabels,
BottomNavigationBarType? type, BottomNavigationBarType? type,
bool? enableFeedback, bool? enableFeedback,
BottomNavigationBarLandscapeLayout? landscapeLayout BottomNavigationBarLandscapeLayout? landscapeLayout,
MaterialStateProperty<MouseCursor?>? mouseCursor,
}) { }) {
return BottomNavigationBarThemeData( return BottomNavigationBarThemeData(
backgroundColor: backgroundColor ?? this.backgroundColor, backgroundColor: backgroundColor ?? this.backgroundColor,
...@@ -155,6 +161,7 @@ class BottomNavigationBarThemeData with Diagnosticable { ...@@ -155,6 +161,7 @@ class BottomNavigationBarThemeData with Diagnosticable {
type: type ?? this.type, type: type ?? this.type,
enableFeedback: enableFeedback ?? this.enableFeedback, enableFeedback: enableFeedback ?? this.enableFeedback,
landscapeLayout: landscapeLayout ?? this.landscapeLayout, landscapeLayout: landscapeLayout ?? this.landscapeLayout,
mouseCursor: mouseCursor ?? this.mouseCursor,
); );
} }
...@@ -179,6 +186,7 @@ class BottomNavigationBarThemeData with Diagnosticable { ...@@ -179,6 +186,7 @@ class BottomNavigationBarThemeData with Diagnosticable {
type: t < 0.5 ? a?.type : b?.type, type: t < 0.5 ? a?.type : b?.type,
enableFeedback: t < 0.5 ? a?.enableFeedback : b?.enableFeedback, enableFeedback: t < 0.5 ? a?.enableFeedback : b?.enableFeedback,
landscapeLayout: t < 0.5 ? a?.landscapeLayout : b?.landscapeLayout, landscapeLayout: t < 0.5 ? a?.landscapeLayout : b?.landscapeLayout,
mouseCursor: t < 0.5 ? a?.mouseCursor : b?.mouseCursor,
); );
} }
...@@ -198,6 +206,7 @@ class BottomNavigationBarThemeData with Diagnosticable { ...@@ -198,6 +206,7 @@ class BottomNavigationBarThemeData with Diagnosticable {
type, type,
enableFeedback, enableFeedback,
landscapeLayout, landscapeLayout,
mouseCursor,
); );
} }
...@@ -220,7 +229,8 @@ class BottomNavigationBarThemeData with Diagnosticable { ...@@ -220,7 +229,8 @@ class BottomNavigationBarThemeData with Diagnosticable {
&& other.showUnselectedLabels == showUnselectedLabels && other.showUnselectedLabels == showUnselectedLabels
&& other.type == type && other.type == type
&& other.enableFeedback == enableFeedback && other.enableFeedback == enableFeedback
&& other.landscapeLayout == landscapeLayout; && other.landscapeLayout == landscapeLayout
&& other.mouseCursor == mouseCursor;
} }
@override @override
...@@ -239,6 +249,7 @@ class BottomNavigationBarThemeData with Diagnosticable { ...@@ -239,6 +249,7 @@ class BottomNavigationBarThemeData with Diagnosticable {
properties.add(DiagnosticsProperty<BottomNavigationBarType>('type', type, defaultValue: null)); properties.add(DiagnosticsProperty<BottomNavigationBarType>('type', type, defaultValue: null));
properties.add(DiagnosticsProperty<bool>('enableFeedback', enableFeedback, defaultValue: null)); properties.add(DiagnosticsProperty<bool>('enableFeedback', enableFeedback, defaultValue: null));
properties.add(DiagnosticsProperty<BottomNavigationBarLandscapeLayout>('landscapeLayout', landscapeLayout, defaultValue: null)); properties.add(DiagnosticsProperty<BottomNavigationBarLandscapeLayout>('landscapeLayout', landscapeLayout, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<MouseCursor?>>('mouseCursor', mouseCursor, defaultValue: null));
} }
} }
......
...@@ -2,8 +2,10 @@ ...@@ -2,8 +2,10 @@
// 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/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:vector_math/vector_math_64.dart' show Vector3; import 'package:vector_math/vector_math_64.dart' show Vector3;
...@@ -28,6 +30,7 @@ void main() { ...@@ -28,6 +30,7 @@ void main() {
expect(themeData.showUnselectedLabels, null); expect(themeData.showUnselectedLabels, null);
expect(themeData.type, null); expect(themeData.type, null);
expect(themeData.landscapeLayout, null); expect(themeData.landscapeLayout, null);
expect(themeData.mouseCursor, null);
const BottomNavigationBarTheme theme = BottomNavigationBarTheme(data: BottomNavigationBarThemeData(), child: SizedBox()); const BottomNavigationBarTheme theme = BottomNavigationBarTheme(data: BottomNavigationBarThemeData(), child: SizedBox());
expect(theme.data.backgroundColor, null); expect(theme.data.backgroundColor, null);
...@@ -42,6 +45,7 @@ void main() { ...@@ -42,6 +45,7 @@ void main() {
expect(theme.data.showUnselectedLabels, null); expect(theme.data.showUnselectedLabels, null);
expect(theme.data.type, null); expect(theme.data.type, null);
expect(themeData.landscapeLayout, null); expect(themeData.landscapeLayout, null);
expect(themeData.mouseCursor, null);
}); });
testWidgets('Default BottomNavigationBarThemeData debugFillProperties', (WidgetTester tester) async { testWidgets('Default BottomNavigationBarThemeData debugFillProperties', (WidgetTester tester) async {
...@@ -70,6 +74,7 @@ void main() { ...@@ -70,6 +74,7 @@ void main() {
showSelectedLabels: true, showSelectedLabels: true,
showUnselectedLabels: true, showUnselectedLabels: true,
type: BottomNavigationBarType.fixed, type: BottomNavigationBarType.fixed,
mouseCursor: MaterialStateMouseCursor.clickable,
).debugFillProperties(builder); ).debugFillProperties(builder);
final List<String> description = builder.properties final List<String> description = builder.properties
...@@ -93,6 +98,7 @@ void main() { ...@@ -93,6 +98,7 @@ void main() {
expect(description[8], 'showSelectedLabels: true'); expect(description[8], 'showSelectedLabels: true');
expect(description[9], 'showUnselectedLabels: true'); expect(description[9], 'showUnselectedLabels: true');
expect(description[10], 'type: BottomNavigationBarType.fixed'); expect(description[10], 'type: BottomNavigationBarType.fixed');
expect(description[11], 'mouseCursor: MaterialStateMouseCursor(clickable)');
}); });
testWidgets('BottomNavigationBar is themeable', (WidgetTester tester) async { testWidgets('BottomNavigationBar is themeable', (WidgetTester tester) async {
...@@ -108,7 +114,7 @@ void main() { ...@@ -108,7 +114,7 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
theme: ThemeData( theme: ThemeData(
bottomNavigationBarTheme: const BottomNavigationBarThemeData( bottomNavigationBarTheme: BottomNavigationBarThemeData(
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
selectedItemColor: selectedItemColor, selectedItemColor: selectedItemColor,
unselectedItemColor: unselectedItemColor, unselectedItemColor: unselectedItemColor,
...@@ -120,6 +126,12 @@ void main() { ...@@ -120,6 +126,12 @@ void main() {
type: BottomNavigationBarType.fixed, type: BottomNavigationBarType.fixed,
selectedLabelStyle: selectedTextStyle, selectedLabelStyle: selectedTextStyle,
unselectedLabelStyle: unselectedTextStyle, unselectedLabelStyle: unselectedTextStyle,
mouseCursor: MaterialStateProperty.resolveWith<MouseCursor?>((Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
return SystemMouseCursors.grab;
}
return SystemMouseCursors.move;
}),
), ),
), ),
home: Scaffold( home: Scaffold(
...@@ -166,6 +178,24 @@ void main() { ...@@ -166,6 +178,24 @@ void main() {
expect(findFadeTransition, findsNothing); expect(findFadeTransition, findsNothing);
expect(_material(tester).elevation, equals(elevation)); expect(_material(tester).elevation, equals(elevation));
expect(_material(tester).color, equals(backgroundColor)); 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 { testWidgets('BottomNavigationBar properties are taken over the theme values', (WidgetTester tester) async {
...@@ -178,6 +208,7 @@ void main() { ...@@ -178,6 +208,7 @@ void main() {
const TextStyle themeUnselectedTextStyle = TextStyle(fontSize: 21); const TextStyle themeUnselectedTextStyle = TextStyle(fontSize: 21);
const double themeElevation = 9.0; const double themeElevation = 9.0;
const BottomNavigationBarLandscapeLayout themeLandscapeLayout = BottomNavigationBarLandscapeLayout.centered; const BottomNavigationBarLandscapeLayout themeLandscapeLayout = BottomNavigationBarLandscapeLayout.centered;
const MaterialStateMouseCursor themeCursor = MaterialStateMouseCursor.clickable;
const Color backgroundColor = Color(0xFF000004); const Color backgroundColor = Color(0xFF000004);
const Color selectedItemColor = Color(0xFF000005); const Color selectedItemColor = Color(0xFF000005);
...@@ -188,6 +219,7 @@ void main() { ...@@ -188,6 +219,7 @@ void main() {
const TextStyle unselectedTextStyle = TextStyle(fontSize: 26); const TextStyle unselectedTextStyle = TextStyle(fontSize: 26);
const double elevation = 7.0; const double elevation = 7.0;
const BottomNavigationBarLandscapeLayout landscapeLayout = BottomNavigationBarLandscapeLayout.spread; const BottomNavigationBarLandscapeLayout landscapeLayout = BottomNavigationBarLandscapeLayout.spread;
const MaterialStateMouseCursor cursor = MaterialStateMouseCursor.textable;
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
...@@ -205,6 +237,7 @@ void main() { ...@@ -205,6 +237,7 @@ void main() {
selectedLabelStyle: themeSelectedTextStyle, selectedLabelStyle: themeSelectedTextStyle,
unselectedLabelStyle: themeUnselectedTextStyle, unselectedLabelStyle: themeUnselectedTextStyle,
landscapeLayout: themeLandscapeLayout, landscapeLayout: themeLandscapeLayout,
mouseCursor: themeCursor,
), ),
), ),
home: Scaffold( home: Scaffold(
...@@ -221,6 +254,7 @@ void main() { ...@@ -221,6 +254,7 @@ void main() {
selectedLabelStyle: selectedTextStyle, selectedLabelStyle: selectedTextStyle,
unselectedLabelStyle: unselectedTextStyle, unselectedLabelStyle: unselectedTextStyle,
landscapeLayout: landscapeLayout, landscapeLayout: landscapeLayout,
mouseCursor: cursor,
items: const <BottomNavigationBarItem>[ items: const <BottomNavigationBarItem>[
BottomNavigationBarItem( BottomNavigationBarItem(
icon: Icon(Icons.ac_unit), icon: Icon(Icons.ac_unit),
...@@ -263,6 +297,17 @@ void main() { ...@@ -263,6 +297,17 @@ void main() {
expect(findFadeTransition, findsNothing); expect(findFadeTransition, findsNothing);
expect(_material(tester).elevation, equals(elevation)); expect(_material(tester).elevation, equals(elevation));
expect(_material(tester).color, equals(backgroundColor)); 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 { testWidgets('BottomNavigationBarTheme can be used to hide all labels', (WidgetTester tester) async {
......
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