Unverified Commit fd6547e1 authored by Taha Tesser's avatar Taha Tesser Committed by GitHub

ListTile: add themeable mouse cursor (#96740)

parent 3a98d8f9
...@@ -99,6 +99,7 @@ class ListTileThemeData with Diagnosticable { ...@@ -99,6 +99,7 @@ class ListTileThemeData with Diagnosticable {
this.minVerticalPadding, this.minVerticalPadding,
this.minLeadingWidth, this.minLeadingWidth,
this.enableFeedback, this.enableFeedback,
this.mouseCursor,
}); });
/// Overrides the default value of [ListTile.dense]. /// Overrides the default value of [ListTile.dense].
...@@ -140,6 +141,9 @@ class ListTileThemeData with Diagnosticable { ...@@ -140,6 +141,9 @@ class ListTileThemeData with Diagnosticable {
/// Overrides the default value of [ListTile.enableFeedback]. /// Overrides the default value of [ListTile.enableFeedback].
final bool? enableFeedback; final bool? enableFeedback;
/// If specified, overrides the default value of [ListTile.mouseCursor].
final MaterialStateProperty<MouseCursor?>? mouseCursor;
/// Creates a copy of this object with the given fields replaced with the /// Creates a copy of this object with the given fields replaced with the
/// new values. /// new values.
ListTileThemeData copyWith({ ListTileThemeData copyWith({
...@@ -156,6 +160,7 @@ class ListTileThemeData with Diagnosticable { ...@@ -156,6 +160,7 @@ class ListTileThemeData with Diagnosticable {
double? minVerticalPadding, double? minVerticalPadding,
double? minLeadingWidth, double? minLeadingWidth,
bool? enableFeedback, bool? enableFeedback,
MaterialStateProperty<MouseCursor?>? mouseCursor,
}) { }) {
return ListTileThemeData( return ListTileThemeData(
dense: dense ?? this.dense, dense: dense ?? this.dense,
...@@ -171,6 +176,7 @@ class ListTileThemeData with Diagnosticable { ...@@ -171,6 +176,7 @@ class ListTileThemeData with Diagnosticable {
minVerticalPadding: minVerticalPadding ?? this.minVerticalPadding, minVerticalPadding: minVerticalPadding ?? this.minVerticalPadding,
minLeadingWidth: minLeadingWidth ?? this.minLeadingWidth, minLeadingWidth: minLeadingWidth ?? this.minLeadingWidth,
enableFeedback: enableFeedback ?? this.enableFeedback, enableFeedback: enableFeedback ?? this.enableFeedback,
mouseCursor: mouseCursor ?? this.mouseCursor,
); );
} }
...@@ -193,6 +199,7 @@ class ListTileThemeData with Diagnosticable { ...@@ -193,6 +199,7 @@ class ListTileThemeData with Diagnosticable {
minVerticalPadding: lerpDouble(a?.minVerticalPadding, b?.minVerticalPadding, t), minVerticalPadding: lerpDouble(a?.minVerticalPadding, b?.minVerticalPadding, t),
minLeadingWidth: lerpDouble(a?.minLeadingWidth, b?.minLeadingWidth, t), minLeadingWidth: lerpDouble(a?.minLeadingWidth, b?.minLeadingWidth, t),
enableFeedback: t < 0.5 ? a?.enableFeedback : b?.enableFeedback, enableFeedback: t < 0.5 ? a?.enableFeedback : b?.enableFeedback,
mouseCursor: t < 0.5 ? a?.mouseCursor : b?.mouseCursor,
); );
} }
...@@ -212,6 +219,7 @@ class ListTileThemeData with Diagnosticable { ...@@ -212,6 +219,7 @@ class ListTileThemeData with Diagnosticable {
minVerticalPadding, minVerticalPadding,
minLeadingWidth, minLeadingWidth,
enableFeedback, enableFeedback,
mouseCursor,
); );
} }
...@@ -234,7 +242,8 @@ class ListTileThemeData with Diagnosticable { ...@@ -234,7 +242,8 @@ class ListTileThemeData with Diagnosticable {
&& other.horizontalTitleGap == horizontalTitleGap && other.horizontalTitleGap == horizontalTitleGap
&& other.minVerticalPadding == minVerticalPadding && other.minVerticalPadding == minVerticalPadding
&& other.minLeadingWidth == minLeadingWidth && other.minLeadingWidth == minLeadingWidth
&& other.enableFeedback == enableFeedback; && other.enableFeedback == enableFeedback
&& other.mouseCursor == mouseCursor;
} }
@override @override
...@@ -253,6 +262,7 @@ class ListTileThemeData with Diagnosticable { ...@@ -253,6 +262,7 @@ class ListTileThemeData with Diagnosticable {
properties.add(DoubleProperty('minVerticalPadding', minVerticalPadding, defaultValue: null)); properties.add(DoubleProperty('minVerticalPadding', minVerticalPadding, defaultValue: null));
properties.add(DoubleProperty('minLeadingWidth', minLeadingWidth, defaultValue: null)); properties.add(DoubleProperty('minLeadingWidth', minLeadingWidth, defaultValue: null));
properties.add(DiagnosticsProperty<bool>('enableFeedback', enableFeedback, defaultValue: null)); properties.add(DiagnosticsProperty<bool>('enableFeedback', enableFeedback, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<MouseCursor?>>('mouseCursor', mouseCursor, defaultValue: null));
} }
} }
...@@ -283,6 +293,7 @@ class ListTileTheme extends InheritedTheme { ...@@ -283,6 +293,7 @@ class ListTileTheme extends InheritedTheme {
Color? tileColor, Color? tileColor,
Color? selectedTileColor, Color? selectedTileColor,
bool? enableFeedback, bool? enableFeedback,
MaterialStateProperty<MouseCursor?>? mouseCursor,
double? horizontalTitleGap, double? horizontalTitleGap,
double? minVerticalPadding, double? minVerticalPadding,
double? minLeadingWidth, double? minLeadingWidth,
...@@ -297,6 +308,7 @@ class ListTileTheme extends InheritedTheme { ...@@ -297,6 +308,7 @@ class ListTileTheme extends InheritedTheme {
tileColor ?? tileColor ??
selectedTileColor ?? selectedTileColor ??
enableFeedback ?? enableFeedback ??
mouseCursor ??
horizontalTitleGap ?? horizontalTitleGap ??
minVerticalPadding ?? minVerticalPadding ??
minLeadingWidth) == null), minLeadingWidth) == null),
...@@ -311,6 +323,7 @@ class ListTileTheme extends InheritedTheme { ...@@ -311,6 +323,7 @@ class ListTileTheme extends InheritedTheme {
_tileColor = tileColor, _tileColor = tileColor,
_selectedTileColor = selectedTileColor, _selectedTileColor = selectedTileColor,
_enableFeedback = enableFeedback, _enableFeedback = enableFeedback,
_mouseCursor = mouseCursor,
_horizontalTitleGap = horizontalTitleGap, _horizontalTitleGap = horizontalTitleGap,
_minVerticalPadding = minVerticalPadding, _minVerticalPadding = minVerticalPadding,
_minLeadingWidth = minLeadingWidth, _minLeadingWidth = minLeadingWidth,
...@@ -330,6 +343,7 @@ class ListTileTheme extends InheritedTheme { ...@@ -330,6 +343,7 @@ class ListTileTheme extends InheritedTheme {
final double? _minVerticalPadding; final double? _minVerticalPadding;
final double? _minLeadingWidth; final double? _minLeadingWidth;
final bool? _enableFeedback; final bool? _enableFeedback;
final MaterialStateProperty<MouseCursor?>? _mouseCursor;
/// The configuration of this theme. /// The configuration of this theme.
ListTileThemeData get data { ListTileThemeData get data {
...@@ -344,6 +358,7 @@ class ListTileTheme extends InheritedTheme { ...@@ -344,6 +358,7 @@ class ListTileTheme extends InheritedTheme {
tileColor: _tileColor, tileColor: _tileColor,
selectedTileColor: _selectedTileColor, selectedTileColor: _selectedTileColor,
enableFeedback: _enableFeedback, enableFeedback: _enableFeedback,
mouseCursor: _mouseCursor,
horizontalTitleGap: _horizontalTitleGap, horizontalTitleGap: _horizontalTitleGap,
minVerticalPadding: _minVerticalPadding, minVerticalPadding: _minVerticalPadding,
minLeadingWidth: _minLeadingWidth, minLeadingWidth: _minLeadingWidth,
...@@ -951,6 +966,7 @@ class ListTile extends StatelessWidget { ...@@ -951,6 +966,7 @@ class ListTile extends StatelessWidget {
/// Inoperative if [enabled] is false. /// Inoperative if [enabled] is false.
final GestureLongPressCallback? onLongPress; final GestureLongPressCallback? onLongPress;
/// {@template flutter.material.ListTile.mouseCursor}
/// 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
/// widget. /// widget.
/// ///
...@@ -959,8 +975,15 @@ class ListTile extends StatelessWidget { ...@@ -959,8 +975,15 @@ class ListTile extends StatelessWidget {
/// ///
/// * [MaterialState.selected]. /// * [MaterialState.selected].
/// * [MaterialState.disabled]. /// * [MaterialState.disabled].
/// {@endtemplate}
/// ///
/// If this property is null, [MaterialStateMouseCursor.clickable] will be used. /// If null, then the value of [ListTileThemeData.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;
/// If this tile is also [enabled] then icons and text are rendered with the same color. /// If this tile is also [enabled] then icons and text are rendered with the same color.
...@@ -1203,19 +1226,20 @@ class ListTile extends StatelessWidget { ...@@ -1203,19 +1226,20 @@ class ListTile extends StatelessWidget {
?? tileTheme.contentPadding?.resolve(textDirection) ?? tileTheme.contentPadding?.resolve(textDirection)
?? defaultContentPadding; ?? defaultContentPadding;
final MouseCursor resolvedMouseCursor = MaterialStateProperty.resolveAs<MouseCursor>( final Set<MaterialState> states = <MaterialState>{
mouseCursor ?? MaterialStateMouseCursor.clickable, if (!enabled || (onTap == null && onLongPress == null)) MaterialState.disabled,
<MaterialState>{ if (selected) MaterialState.selected,
if (!enabled || (onTap == null && onLongPress == null)) MaterialState.disabled, };
if (selected) MaterialState.selected,
}, final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor?>(mouseCursor, states)
); ?? tileTheme.mouseCursor?.resolve(states)
?? MaterialStateMouseCursor.clickable.resolve(states);
return InkWell( return InkWell(
customBorder: shape ?? tileTheme.shape, customBorder: shape ?? tileTheme.shape,
onTap: enabled ? onTap : null, onTap: enabled ? onTap : null,
onLongPress: enabled ? onLongPress : null, onLongPress: enabled ? onLongPress : null,
mouseCursor: resolvedMouseCursor, mouseCursor: effectiveMouseCursor,
canRequestFocus: enabled, canRequestFocus: enabled,
focusNode: focusNode, focusNode: focusNode,
focusColor: focusColor, focusColor: focusColor,
......
...@@ -71,6 +71,7 @@ void main() { ...@@ -71,6 +71,7 @@ void main() {
expect(themeData.minVerticalPadding, null); expect(themeData.minVerticalPadding, null);
expect(themeData.minLeadingWidth, null); expect(themeData.minLeadingWidth, null);
expect(themeData.enableFeedback, null); expect(themeData.enableFeedback, null);
expect(themeData.mouseCursor, null);
}); });
testWidgets('Default ListTileThemeData debugFillProperties', (WidgetTester tester) async { testWidgets('Default ListTileThemeData debugFillProperties', (WidgetTester tester) async {
...@@ -101,6 +102,7 @@ void main() { ...@@ -101,6 +102,7 @@ void main() {
minVerticalPadding: 300, minVerticalPadding: 300,
minLeadingWidth: 400, minLeadingWidth: 400,
enableFeedback: true, enableFeedback: true,
mouseCursor: MaterialStateMouseCursor.clickable,
).debugFillProperties(builder); ).debugFillProperties(builder);
final List<String> description = builder.properties final List<String> description = builder.properties
...@@ -121,7 +123,8 @@ void main() { ...@@ -121,7 +123,8 @@ void main() {
'horizontalTitleGap: 200.0', 'horizontalTitleGap: 200.0',
'minVerticalPadding: 300.0', 'minVerticalPadding: 300.0',
'minLeadingWidth: 400.0', 'minLeadingWidth: 400.0',
'enableFeedback: true' 'enableFeedback: true',
'mouseCursor: MaterialStateMouseCursor(clickable)',
]); ]);
}); });
...@@ -145,6 +148,7 @@ void main() { ...@@ -145,6 +148,7 @@ void main() {
minVerticalPadding: 300, minVerticalPadding: 300,
minLeadingWidth: 400, minLeadingWidth: 400,
enableFeedback: true, enableFeedback: true,
mouseCursor: MaterialStateMouseCursor.clickable,
child: Center( child: Center(
child: Builder( child: Builder(
builder: (BuildContext context) { builder: (BuildContext context) {
...@@ -171,6 +175,7 @@ void main() { ...@@ -171,6 +175,7 @@ void main() {
expect(theme.minVerticalPadding, 300); expect(theme.minVerticalPadding, 300);
expect(theme.minLeadingWidth, 400); expect(theme.minLeadingWidth, 400);
expect(theme.enableFeedback, true); expect(theme.enableFeedback, true);
expect(theme.mouseCursor, MaterialStateMouseCursor.clickable);
}); });
testWidgets('ListTile geometry (LTR)', (WidgetTester tester) async { testWidgets('ListTile geometry (LTR)', (WidgetTester tester) async {
...@@ -424,6 +429,13 @@ void main() { ...@@ -424,6 +429,13 @@ void main() {
selectedColor: selectedColor, selectedColor: selectedColor,
iconColor: iconColor, iconColor: iconColor,
textColor: textColor, textColor: textColor,
mouseCursor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return SystemMouseCursors.forbidden;
}
return SystemMouseCursors.click;
}),
), ),
child: Builder( child: Builder(
builder: (BuildContext context) { builder: (BuildContext context) {
...@@ -499,6 +511,16 @@ void main() { ...@@ -499,6 +511,16 @@ void main() {
// A selected ListTile's InkWell gets the ListTileTheme's shape // A selected ListTile's InkWell gets the ListTileTheme's shape
await tester.pumpWidget(buildFrame(selected: true, shape: roundedShape)); await tester.pumpWidget(buildFrame(selected: true, shape: roundedShape));
expect(inkWellBorder(), roundedShape); expect(inkWellBorder(), roundedShape);
// Cursor updates when hovering disabled ListTile
await tester.pumpWidget(buildFrame(enabled: false));
final Offset listTile = tester.getCenter(find.byKey(titleKey));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
addTearDown(gesture.removePointer);
await gesture.moveTo(listTile);
await tester.pumpAndSettle();
expect(RendererBinding.instance!.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.forbidden);
}); });
testWidgets('ListTile semantics', (WidgetTester tester) async { testWidgets('ListTile semantics', (WidgetTester tester) async {
...@@ -2071,6 +2093,34 @@ void main() { ...@@ -2071,6 +2093,34 @@ void main() {
expect(feedback.clickSoundCount, 1); expect(feedback.clickSoundCount, 1);
expect(feedback.hapticCount, 0); expect(feedback.hapticCount, 0);
}); });
testWidgets('ListTile.mouseCursor overrides ListTileTheme.mouseCursor', (WidgetTester tester) async {
final Key tileKey = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: ListTileTheme(
data: const ListTileThemeData(mouseCursor: MaterialStateMouseCursor.clickable),
child: ListTile(
key: tileKey,
mouseCursor: MaterialStateMouseCursor.textable,
title: const Text('Title'),
onTap: () {},
),
),
),
),
);
final Offset listTile = tester.getCenter(find.byKey(tileKey));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
addTearDown(gesture.removePointer);
await gesture.moveTo(listTile);
await tester.pumpAndSettle();
expect(RendererBinding.instance!.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
});
}); });
testWidgets('ListTile horizontalTitleGap = 0.0', (WidgetTester tester) async { testWidgets('ListTile horizontalTitleGap = 0.0', (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