Unverified Commit c24b2c3c authored by Hans Muller's avatar Hans Muller Committed by GitHub

Slider: add themeable mouse cursor v2 (#96623)

parent e78f135f
...@@ -373,17 +373,26 @@ class Slider extends StatefulWidget { ...@@ -373,17 +373,26 @@ class Slider extends StatefulWidget {
/// (like the native default iOS slider). /// (like the native default iOS slider).
final Color? thumbColor; final Color? thumbColor;
/// {@template flutter.material.slider.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.
/// ///
/// If [mouseCursor] is a [MaterialStateProperty<MouseCursor>], /// If [mouseCursor] is a [MaterialStateProperty<MouseCursor>],
/// [MaterialStateProperty.resolve] is used for the following [MaterialState]s: /// [MaterialStateProperty.resolve] is used for the following [MaterialState]s:
/// ///
/// * [MaterialState.dragged].
/// * [MaterialState.hovered]. /// * [MaterialState.hovered].
/// * [MaterialState.focused]. /// * [MaterialState.focused].
/// * [MaterialState.disabled]. /// * [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; final MouseCursor? mouseCursor;
/// The callback used to create a semantic value from a slider value. /// The callback used to create a semantic value from a slider value.
...@@ -481,6 +490,8 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin { ...@@ -481,6 +490,8 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
// Value Indicator Animation that appears on the Overlay. // Value Indicator Animation that appears on the Overlay.
PaintValueIndicator? paintValueIndicator; PaintValueIndicator? paintValueIndicator;
bool _dragging = false;
FocusNode? _focusNode; FocusNode? _focusNode;
FocusNode get focusNode => widget.focusNode ?? _focusNode!; FocusNode get focusNode => widget.focusNode ?? _focusNode!;
...@@ -540,13 +551,13 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin { ...@@ -540,13 +551,13 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
} }
void _handleDragStart(double value) { void _handleDragStart(double value) {
assert(widget.onChangeStart != null); _dragging = true;
widget.onChangeStart!(_lerp(value)); widget.onChangeStart?.call(_lerp(value));
} }
void _handleDragEnd(double value) { void _handleDragEnd(double value) {
assert(widget.onChangeEnd != null); _dragging = false;
widget.onChangeEnd!(_lerp(value)); widget.onChangeEnd?.call(_lerp(value));
} }
void _actionHandler(_AdjustSliderIntent intent) { void _actionHandler(_AdjustSliderIntent intent) {
...@@ -692,14 +703,15 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin { ...@@ -692,14 +703,15 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
color: theme.colorScheme.onPrimary, color: theme.colorScheme.onPrimary,
), ),
); );
final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor>( final Set<MaterialState> states = <MaterialState>{
widget.mouseCursor ?? MaterialStateMouseCursor.clickable, if (!_enabled) MaterialState.disabled,
<MaterialState>{ if (_hovering) MaterialState.hovered,
if (!_enabled) MaterialState.disabled, if (_focused) MaterialState.focused,
if (_hovering) MaterialState.hovered, if (_dragging) MaterialState.dragged,
if (_focused) MaterialState.focused, };
}, 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 // 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 // indicators It must be kept in sync with the function with the same name
...@@ -748,8 +760,8 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin { ...@@ -748,8 +760,8 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
textScaleFactor: MediaQuery.of(context).textScaleFactor, textScaleFactor: MediaQuery.of(context).textScaleFactor,
screenSize: _screenSize(), screenSize: _screenSize(),
onChanged: (widget.onChanged != null) && (widget.max > widget.min) ? _handleChanged : null, onChanged: (widget.onChanged != null) && (widget.max > widget.min) ? _handleChanged : null,
onChangeStart: widget.onChangeStart != null ? _handleDragStart : null, onChangeStart: _handleDragStart,
onChangeEnd: widget.onChangeEnd != null ? _handleDragEnd : null, onChangeEnd: _handleDragEnd,
state: this, state: this,
semanticFormatterCallback: widget.semanticFormatterCallback, semanticFormatterCallback: widget.semanticFormatterCallback,
hasFocus: _focused, hasFocus: _focused,
......
...@@ -10,6 +10,7 @@ import 'package:flutter/rendering.dart'; ...@@ -10,6 +10,7 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'colors.dart'; import 'colors.dart';
import 'material_state.dart';
import 'theme.dart'; import 'theme.dart';
/// Applies a slider theme to descendant [Slider] widgets. /// Applies a slider theme to descendant [Slider] widgets.
...@@ -290,6 +291,7 @@ class SliderThemeData with Diagnosticable { ...@@ -290,6 +291,7 @@ class SliderThemeData with Diagnosticable {
this.valueIndicatorTextStyle, this.valueIndicatorTextStyle,
this.minThumbSeparation, this.minThumbSeparation,
this.thumbSelector, this.thumbSelector,
this.mouseCursor,
}); });
/// Generates a SliderThemeData from three main colors. /// Generates a SliderThemeData from three main colors.
...@@ -561,6 +563,11 @@ class SliderThemeData with Diagnosticable { ...@@ -561,6 +563,11 @@ class SliderThemeData with Diagnosticable {
/// Override this for custom thumb selection. /// Override this for custom thumb selection.
final RangeThumbSelector? thumbSelector; 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 /// Creates a copy of this object but with the given fields replaced with the
/// new values. /// new values.
SliderThemeData copyWith({ SliderThemeData copyWith({
...@@ -591,6 +598,7 @@ class SliderThemeData with Diagnosticable { ...@@ -591,6 +598,7 @@ class SliderThemeData with Diagnosticable {
TextStyle? valueIndicatorTextStyle, TextStyle? valueIndicatorTextStyle,
double? minThumbSeparation, double? minThumbSeparation,
RangeThumbSelector? thumbSelector, RangeThumbSelector? thumbSelector,
MaterialStateProperty<MouseCursor?>? mouseCursor,
}) { }) {
return SliderThemeData( return SliderThemeData(
trackHeight: trackHeight ?? this.trackHeight, trackHeight: trackHeight ?? this.trackHeight,
...@@ -620,6 +628,7 @@ class SliderThemeData with Diagnosticable { ...@@ -620,6 +628,7 @@ class SliderThemeData with Diagnosticable {
valueIndicatorTextStyle: valueIndicatorTextStyle ?? this.valueIndicatorTextStyle, valueIndicatorTextStyle: valueIndicatorTextStyle ?? this.valueIndicatorTextStyle,
minThumbSeparation: minThumbSeparation ?? this.minThumbSeparation, minThumbSeparation: minThumbSeparation ?? this.minThumbSeparation,
thumbSelector: thumbSelector ?? this.thumbSelector, thumbSelector: thumbSelector ?? this.thumbSelector,
mouseCursor: mouseCursor ?? this.mouseCursor,
); );
} }
...@@ -660,6 +669,7 @@ class SliderThemeData with Diagnosticable { ...@@ -660,6 +669,7 @@ class SliderThemeData with Diagnosticable {
valueIndicatorTextStyle: TextStyle.lerp(a.valueIndicatorTextStyle, b.valueIndicatorTextStyle, t), valueIndicatorTextStyle: TextStyle.lerp(a.valueIndicatorTextStyle, b.valueIndicatorTextStyle, t),
minThumbSeparation: lerpDouble(a.minThumbSeparation, b.minThumbSeparation, t), minThumbSeparation: lerpDouble(a.minThumbSeparation, b.minThumbSeparation, t),
thumbSelector: t < 0.5 ? a.thumbSelector : b.thumbSelector, thumbSelector: t < 0.5 ? a.thumbSelector : b.thumbSelector,
mouseCursor: t < 0.5 ? a.mouseCursor : b.mouseCursor,
); );
} }
...@@ -693,6 +703,7 @@ class SliderThemeData with Diagnosticable { ...@@ -693,6 +703,7 @@ class SliderThemeData with Diagnosticable {
valueIndicatorTextStyle, valueIndicatorTextStyle,
minThumbSeparation, minThumbSeparation,
thumbSelector, thumbSelector,
mouseCursor,
]); ]);
} }
...@@ -731,7 +742,8 @@ class SliderThemeData with Diagnosticable { ...@@ -731,7 +742,8 @@ class SliderThemeData with Diagnosticable {
&& other.showValueIndicator == showValueIndicator && other.showValueIndicator == showValueIndicator
&& other.valueIndicatorTextStyle == valueIndicatorTextStyle && other.valueIndicatorTextStyle == valueIndicatorTextStyle
&& other.minThumbSeparation == minThumbSeparation && other.minThumbSeparation == minThumbSeparation
&& other.thumbSelector == thumbSelector; && other.thumbSelector == thumbSelector
&& other.mouseCursor == mouseCursor;
} }
@override @override
...@@ -765,6 +777,7 @@ class SliderThemeData with Diagnosticable { ...@@ -765,6 +777,7 @@ class SliderThemeData with Diagnosticable {
properties.add(DiagnosticsProperty<TextStyle>('valueIndicatorTextStyle', valueIndicatorTextStyle, defaultValue: defaultData.valueIndicatorTextStyle)); properties.add(DiagnosticsProperty<TextStyle>('valueIndicatorTextStyle', valueIndicatorTextStyle, defaultValue: defaultData.valueIndicatorTextStyle));
properties.add(DoubleProperty('minThumbSeparation', minThumbSeparation, defaultValue: defaultData.minThumbSeparation)); properties.add(DoubleProperty('minThumbSeparation', minThumbSeparation, defaultValue: defaultData.minThumbSeparation));
properties.add(DiagnosticsProperty<RangeThumbSelector>('thumbSelector', thumbSelector, defaultValue: defaultData.thumbSelector)); properties.add(DiagnosticsProperty<RangeThumbSelector>('thumbSelector', thumbSelector, defaultValue: defaultData.thumbSelector));
properties.add(DiagnosticsProperty<MaterialStateProperty<MouseCursor?>>('mouseCursor', mouseCursor, defaultValue: defaultData.mouseCursor));
} }
} }
......
...@@ -70,6 +70,35 @@ class TallSliderTickMarkShape extends SliderTickMarkShape { ...@@ -70,6 +70,35 @@ class TallSliderTickMarkShape extends SliderTickMarkShape {
} }
} }
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() { void main() {
testWidgets('Slider can move when tapped (LTR)', (WidgetTester tester) async { testWidgets('Slider can move when tapped (LTR)', (WidgetTester tester) async {
final Key sliderKey = UniqueKey(); final Key sliderKey = UniqueKey();
...@@ -2521,6 +2550,57 @@ void main() { ...@@ -2521,6 +2550,57 @@ void main() {
expect(RendererBinding.instance!.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click); 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 { testWidgets('Slider implements debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'dart:ui' show window; import 'dart:ui' show window;
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_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -57,6 +58,7 @@ void main() { ...@@ -57,6 +58,7 @@ void main() {
rangeValueIndicatorShape: PaddleRangeSliderValueIndicatorShape(), rangeValueIndicatorShape: PaddleRangeSliderValueIndicatorShape(),
showValueIndicator: ShowValueIndicator.always, showValueIndicator: ShowValueIndicator.always,
valueIndicatorTextStyle: TextStyle(color: Colors.black), valueIndicatorTextStyle: TextStyle(color: Colors.black),
mouseCursor: MaterialStateMouseCursor.clickable,
).debugFillProperties(builder); ).debugFillProperties(builder);
final List<String> description = builder.properties final List<String> description = builder.properties
...@@ -90,6 +92,7 @@ void main() { ...@@ -90,6 +92,7 @@ void main() {
"rangeValueIndicatorShape: Instance of 'PaddleRangeSliderValueIndicatorShape'", "rangeValueIndicatorShape: Instance of 'PaddleRangeSliderValueIndicatorShape'",
'showValueIndicator: always', 'showValueIndicator: always',
'valueIndicatorTextStyle: TextStyle(inherit: true, color: Color(0xff000000))', 'valueIndicatorTextStyle: TextStyle(inherit: true, color: Color(0xff000000))',
'mouseCursor: MaterialStateMouseCursor(clickable)'
]); ]);
}); });
...@@ -1242,6 +1245,21 @@ void main() { ...@@ -1242,6 +1245,21 @@ void main() {
); );
}); });
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 { class RoundedRectSliderTrackShapeWithCustomAdditionalActiveTrackHeight extends RoundedRectSliderTrackShape {
......
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