Slider: add themeable mouse cursor v2 (#96623)
diff --git a/packages/flutter/lib/src/material/slider.dart b/packages/flutter/lib/src/material/slider.dart
index f5668a5..d317891 100644
--- a/packages/flutter/lib/src/material/slider.dart
+++ b/packages/flutter/lib/src/material/slider.dart
@@ -373,17 +373,26 @@
/// (like the native default iOS slider).
final Color? thumbColor;
+ /// {@template flutter.material.slider.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]s:
///
+ /// * [MaterialState.dragged].
/// * [MaterialState.hovered].
/// * [MaterialState.focused].
/// * [MaterialState.disabled].
+ /// {@endtemplate}
///
- /// If this property is null, [MaterialStateMouseCursor.clickable] will be used.
+ /// If null, then the value of [SliderThemeData.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;
/// The callback used to create a semantic value from a slider value.
@@ -481,6 +490,8 @@
// Value Indicator Animation that appears on the Overlay.
PaintValueIndicator? paintValueIndicator;
+ bool _dragging = false;
+
FocusNode? _focusNode;
FocusNode get focusNode => widget.focusNode ?? _focusNode!;
@@ -540,13 +551,13 @@
}
void _handleDragStart(double value) {
- assert(widget.onChangeStart != null);
- widget.onChangeStart!(_lerp(value));
+ _dragging = true;
+ widget.onChangeStart?.call(_lerp(value));
}
void _handleDragEnd(double value) {
- assert(widget.onChangeEnd != null);
- widget.onChangeEnd!(_lerp(value));
+ _dragging = false;
+ widget.onChangeEnd?.call(_lerp(value));
}
void _actionHandler(_AdjustSliderIntent intent) {
@@ -692,14 +703,15 @@
color: theme.colorScheme.onPrimary,
),
);
- final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor>(
- widget.mouseCursor ?? MaterialStateMouseCursor.clickable,
- <MaterialState>{
- if (!_enabled) MaterialState.disabled,
- if (_hovering) MaterialState.hovered,
- if (_focused) MaterialState.focused,
- },
- );
+ final Set<MaterialState> states = <MaterialState>{
+ if (!_enabled) MaterialState.disabled,
+ if (_hovering) MaterialState.hovered,
+ if (_focused) MaterialState.focused,
+ if (_dragging) MaterialState.dragged,
+ };
+ final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, states)
+ ?? sliderTheme.mouseCursor?.resolve(states)
+ ?? MaterialStateMouseCursor.clickable.resolve(states);
// This size is used as the max bounds for the painting of the value
// indicators It must be kept in sync with the function with the same name
@@ -748,8 +760,8 @@
textScaleFactor: MediaQuery.of(context).textScaleFactor,
screenSize: _screenSize(),
onChanged: (widget.onChanged != null) && (widget.max > widget.min) ? _handleChanged : null,
- onChangeStart: widget.onChangeStart != null ? _handleDragStart : null,
- onChangeEnd: widget.onChangeEnd != null ? _handleDragEnd : null,
+ onChangeStart: _handleDragStart,
+ onChangeEnd: _handleDragEnd,
state: this,
semanticFormatterCallback: widget.semanticFormatterCallback,
hasFocus: _focused,
diff --git a/packages/flutter/lib/src/material/slider_theme.dart b/packages/flutter/lib/src/material/slider_theme.dart
index 6def4b6..9b42d09 100644
--- a/packages/flutter/lib/src/material/slider_theme.dart
+++ b/packages/flutter/lib/src/material/slider_theme.dart
@@ -10,6 +10,7 @@
import 'package:flutter/widgets.dart';
import 'colors.dart';
+import 'material_state.dart';
import 'theme.dart';
/// Applies a slider theme to descendant [Slider] widgets.
@@ -290,6 +291,7 @@
this.valueIndicatorTextStyle,
this.minThumbSeparation,
this.thumbSelector,
+ this.mouseCursor,
});
/// Generates a SliderThemeData from three main colors.
@@ -561,6 +563,11 @@
/// Override this for custom thumb selection.
final RangeThumbSelector? thumbSelector;
+ /// {@macro flutter.material.slider.mouseCursor}
+ ///
+ /// If specified, overrides the default value of [Slider.mouseCursor].
+ final MaterialStateProperty<MouseCursor?>? mouseCursor;
+
/// Creates a copy of this object but with the given fields replaced with the
/// new values.
SliderThemeData copyWith({
@@ -591,6 +598,7 @@
TextStyle? valueIndicatorTextStyle,
double? minThumbSeparation,
RangeThumbSelector? thumbSelector,
+ MaterialStateProperty<MouseCursor?>? mouseCursor,
}) {
return SliderThemeData(
trackHeight: trackHeight ?? this.trackHeight,
@@ -620,6 +628,7 @@
valueIndicatorTextStyle: valueIndicatorTextStyle ?? this.valueIndicatorTextStyle,
minThumbSeparation: minThumbSeparation ?? this.minThumbSeparation,
thumbSelector: thumbSelector ?? this.thumbSelector,
+ mouseCursor: mouseCursor ?? this.mouseCursor,
);
}
@@ -660,6 +669,7 @@
valueIndicatorTextStyle: TextStyle.lerp(a.valueIndicatorTextStyle, b.valueIndicatorTextStyle, t),
minThumbSeparation: lerpDouble(a.minThumbSeparation, b.minThumbSeparation, t),
thumbSelector: t < 0.5 ? a.thumbSelector : b.thumbSelector,
+ mouseCursor: t < 0.5 ? a.mouseCursor : b.mouseCursor,
);
}
@@ -693,6 +703,7 @@
valueIndicatorTextStyle,
minThumbSeparation,
thumbSelector,
+ mouseCursor,
]);
}
@@ -731,7 +742,8 @@
&& other.showValueIndicator == showValueIndicator
&& other.valueIndicatorTextStyle == valueIndicatorTextStyle
&& other.minThumbSeparation == minThumbSeparation
- && other.thumbSelector == thumbSelector;
+ && other.thumbSelector == thumbSelector
+ && other.mouseCursor == mouseCursor;
}
@override
@@ -765,6 +777,7 @@
properties.add(DiagnosticsProperty<TextStyle>('valueIndicatorTextStyle', valueIndicatorTextStyle, defaultValue: defaultData.valueIndicatorTextStyle));
properties.add(DoubleProperty('minThumbSeparation', minThumbSeparation, defaultValue: defaultData.minThumbSeparation));
properties.add(DiagnosticsProperty<RangeThumbSelector>('thumbSelector', thumbSelector, defaultValue: defaultData.thumbSelector));
+ properties.add(DiagnosticsProperty<MaterialStateProperty<MouseCursor?>>('mouseCursor', mouseCursor, defaultValue: defaultData.mouseCursor));
}
}
diff --git a/packages/flutter/test/material/slider_test.dart b/packages/flutter/test/material/slider_test.dart
index 2476c0a..fc08542 100644
--- a/packages/flutter/test/material/slider_test.dart
+++ b/packages/flutter/test/material/slider_test.dart
@@ -70,6 +70,35 @@
}
}
+class _StateDependentMouseCursor extends MaterialStateMouseCursor {
+ const _StateDependentMouseCursor({
+ this.disabled = SystemMouseCursors.none,
+ this.dragged = SystemMouseCursors.none,
+ this.hovered = SystemMouseCursors.none,
+ });
+
+ final MouseCursor disabled;
+ final MouseCursor hovered;
+ final MouseCursor dragged;
+
+ @override
+ MouseCursor resolve(Set<MaterialState> states) {
+ if (states.contains(MaterialState.disabled)) {
+ return disabled;
+ }
+ if (states.contains(MaterialState.dragged)) {
+ return dragged;
+ }
+ if (states.contains(MaterialState.hovered)) {
+ return hovered;
+ }
+ return SystemMouseCursors.none;
+ }
+
+ @override
+ String get debugDescription => '_StateDependentMouseCursor';
+}
+
void main() {
testWidgets('Slider can move when tapped (LTR)', (WidgetTester tester) async {
final Key sliderKey = UniqueKey();
@@ -2521,6 +2550,57 @@
expect(RendererBinding.instance!.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
});
+ testWidgets('Slider MaterialStateMouseCursor resolves correctly', (WidgetTester tester) async {
+ const MouseCursor disabledCursor = SystemMouseCursors.basic;
+ const MouseCursor hoveredCursor = SystemMouseCursors.grab;
+ const MouseCursor draggedCursor = SystemMouseCursors.move;
+
+ Widget buildFrame({ required bool enabled }) {
+ return MaterialApp(
+ home: Directionality(
+ textDirection: TextDirection.ltr,
+ child: Material(
+ child: Center(
+ child: MouseRegion(
+ cursor: SystemMouseCursors.forbidden,
+ child: Slider(
+ mouseCursor: const _StateDependentMouseCursor(
+ disabled: disabledCursor,
+ hovered: hoveredCursor,
+ dragged: draggedCursor,
+ ),
+ value: 0.5,
+ onChanged: enabled ? (double newValue) { } : null,
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
+ await gesture.addPointer(location: Offset.zero);
+ addTearDown(gesture.removePointer);
+
+ await tester.pumpWidget(buildFrame(enabled: false));
+ expect(RendererBinding.instance!.mouseTracker.debugDeviceActiveCursor(1), disabledCursor);
+
+ await tester.pumpWidget(buildFrame(enabled: true));
+ expect(RendererBinding.instance!.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.none);
+
+ await gesture.moveTo(tester.getCenter(find.byType(Slider))); // start hover
+ await tester.pumpAndSettle();
+ expect(RendererBinding.instance!.mouseTracker.debugDeviceActiveCursor(1), hoveredCursor);
+
+ await tester.timedDrag(
+ find.byType(Slider),
+ const Offset(20.0, 0.0),
+ const Duration(milliseconds: 100),
+ );
+ expect(RendererBinding.instance!.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.move);
+ });
+
testWidgets('Slider implements debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
diff --git a/packages/flutter/test/material/slider_theme_test.dart b/packages/flutter/test/material/slider_theme_test.dart
index 1d00f37..38e8d33 100644
--- a/packages/flutter/test/material/slider_theme_test.dart
+++ b/packages/flutter/test/material/slider_theme_test.dart
@@ -4,6 +4,7 @@
import 'dart:ui' show window;
+import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -57,6 +58,7 @@
rangeValueIndicatorShape: PaddleRangeSliderValueIndicatorShape(),
showValueIndicator: ShowValueIndicator.always,
valueIndicatorTextStyle: TextStyle(color: Colors.black),
+ mouseCursor: MaterialStateMouseCursor.clickable,
).debugFillProperties(builder);
final List<String> description = builder.properties
@@ -90,6 +92,7 @@
"rangeValueIndicatorShape: Instance of 'PaddleRangeSliderValueIndicatorShape'",
'showValueIndicator: always',
'valueIndicatorTextStyle: TextStyle(inherit: true, color: Color(0xff000000))',
+ 'mouseCursor: MaterialStateMouseCursor(clickable)'
]);
});
@@ -1242,6 +1245,21 @@
);
});
+ testWidgets('The mouse cursor is themeable', (WidgetTester tester) async {
+ await tester.pumpWidget(_buildApp(
+ ThemeData().sliderTheme.copyWith(
+ mouseCursor: MaterialStateProperty.all(SystemMouseCursors.text),
+ )
+ ));
+
+ await tester.pumpAndSettle();
+ final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
+ await gesture.addPointer();
+ addTearDown(gesture.removePointer);
+ await gesture.moveTo(tester.getCenter(find.byType(Slider)));
+ await tester.pumpAndSettle();
+ expect(RendererBinding.instance!.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
+ });
}
class RoundedRectSliderTrackShapeWithCustomAdditionalActiveTrackHeight extends RoundedRectSliderTrackShape {