Slider: add themeable mouse cursor v2 (#96623)
This commit is contained in:
@@ -373,17 +373,26 @@ class Slider extends StatefulWidget {
|
||||
/// (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 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
|
||||
// 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 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
|
||||
}
|
||||
|
||||
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 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
|
||||
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 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
|
||||
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,
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:flutter/rendering.dart';
|
||||
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 @@ class SliderThemeData with Diagnosticable {
|
||||
this.valueIndicatorTextStyle,
|
||||
this.minThumbSeparation,
|
||||
this.thumbSelector,
|
||||
this.mouseCursor,
|
||||
});
|
||||
|
||||
/// Generates a SliderThemeData from three main colors.
|
||||
@@ -561,6 +563,11 @@ class SliderThemeData with Diagnosticable {
|
||||
/// 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 @@ class SliderThemeData with Diagnosticable {
|
||||
TextStyle? valueIndicatorTextStyle,
|
||||
double? minThumbSeparation,
|
||||
RangeThumbSelector? thumbSelector,
|
||||
MaterialStateProperty<MouseCursor?>? mouseCursor,
|
||||
}) {
|
||||
return SliderThemeData(
|
||||
trackHeight: trackHeight ?? this.trackHeight,
|
||||
@@ -620,6 +628,7 @@ class SliderThemeData with Diagnosticable {
|
||||
valueIndicatorTextStyle: valueIndicatorTextStyle ?? this.valueIndicatorTextStyle,
|
||||
minThumbSeparation: minThumbSeparation ?? this.minThumbSeparation,
|
||||
thumbSelector: thumbSelector ?? this.thumbSelector,
|
||||
mouseCursor: mouseCursor ?? this.mouseCursor,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -660,6 +669,7 @@ class SliderThemeData with Diagnosticable {
|
||||
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 @@ class SliderThemeData with Diagnosticable {
|
||||
valueIndicatorTextStyle,
|
||||
minThumbSeparation,
|
||||
thumbSelector,
|
||||
mouseCursor,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -731,7 +742,8 @@ class SliderThemeData with Diagnosticable {
|
||||
&& other.showValueIndicator == showValueIndicator
|
||||
&& other.valueIndicatorTextStyle == valueIndicatorTextStyle
|
||||
&& other.minThumbSeparation == minThumbSeparation
|
||||
&& other.thumbSelector == thumbSelector;
|
||||
&& other.thumbSelector == thumbSelector
|
||||
&& other.mouseCursor == mouseCursor;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -765,6 +777,7 @@ class SliderThemeData with Diagnosticable {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
testWidgets('Slider can move when tapped (LTR)', (WidgetTester tester) async {
|
||||
final Key sliderKey = UniqueKey();
|
||||
@@ -2521,6 +2550,57 @@ void main() {
|
||||
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();
|
||||
|
||||
|
||||
@@ -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 @@ void main() {
|
||||
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 @@ void main() {
|
||||
"rangeValueIndicatorShape: Instance of 'PaddleRangeSliderValueIndicatorShape'",
|
||||
'showValueIndicator: always',
|
||||
'valueIndicatorTextStyle: TextStyle(inherit: true, color: Color(0xff000000))',
|
||||
'mouseCursor: MaterialStateMouseCursor(clickable)'
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user