Unverified Commit 9470b9e2 authored by Tong Mu's avatar Tong Mu Committed by GitHub

Add material state mouse cursor to TextField (#59363)

parent 3406870d
...@@ -233,6 +233,14 @@ class _MaterialStateColor extends MaterialStateColor { ...@@ -233,6 +233,14 @@ class _MaterialStateColor extends MaterialStateColor {
/// ///
/// This should only be used as parameters when they are documented to take /// This should only be used as parameters when they are documented to take
/// [MaterialStateMouseCursor], otherwise only the default state will be used. /// [MaterialStateMouseCursor], otherwise only the default state will be used.
///
/// This class also predefines several kinds of material state mouse cursors.
///
/// See also:
///
/// * [MouseCursor] for introduction on the mouse cursor system.
/// * [SystemMouseCursors], which defines cursors that are supported by
/// native platforms.
abstract class MaterialStateMouseCursor extends MouseCursor implements MaterialStateProperty<MouseCursor> { abstract class MaterialStateMouseCursor extends MouseCursor implements MaterialStateProperty<MouseCursor> {
/// Creates a [MaterialStateMouseCursor]. /// Creates a [MaterialStateMouseCursor].
const MaterialStateMouseCursor(); const MaterialStateMouseCursor();
...@@ -257,22 +265,47 @@ abstract class MaterialStateMouseCursor extends MouseCursor implements MaterialS ...@@ -257,22 +265,47 @@ abstract class MaterialStateMouseCursor extends MouseCursor implements MaterialS
/// disabled, the cursor resolves to [SystemMouseCursors.basic]. /// disabled, the cursor resolves to [SystemMouseCursors.basic].
/// ///
/// This cursor is the default for many Material widgets. /// This cursor is the default for many Material widgets.
static const MaterialStateMouseCursor clickable = _ClickableMouseCursor(); static const MaterialStateMouseCursor clickable = _EnabledAndDisabledMouseCursor(
enabledCursor: SystemMouseCursors.click,
disabledCursor: SystemMouseCursors.basic,
name: 'clickable',
);
/// A mouse cursor for material widgets related to text, which resolves differently
/// when the widget is disabled.
///
/// By default this cursor resolves to [SystemMouseCursors.text]. If the widget is
/// disabled, the cursor resolves to [SystemMouseCursors.basic].
///
/// This cursor is the default for many Material widgets.
static const MaterialStateMouseCursor textable = _EnabledAndDisabledMouseCursor(
enabledCursor: SystemMouseCursors.text,
disabledCursor: SystemMouseCursors.basic,
name: 'textable',
);
} }
class _ClickableMouseCursor extends MaterialStateMouseCursor { class _EnabledAndDisabledMouseCursor extends MaterialStateMouseCursor {
const _ClickableMouseCursor(); const _EnabledAndDisabledMouseCursor({
this.enabledCursor,
this.disabledCursor,
this.name,
});
final MouseCursor enabledCursor;
final MouseCursor disabledCursor;
final String name;
@override @override
MouseCursor resolve(Set<MaterialState> states) { MouseCursor resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) { if (states.contains(MaterialState.disabled)) {
return SystemMouseCursors.basic; return disabledCursor;
} }
return SystemMouseCursors.click; return enabledCursor;
} }
@override @override
String get debugDescription => 'MaterialStateMouseCursor(clickable)'; String get debugDescription => 'MaterialStateMouseCursor($name)';
} }
/// Interface for classes that can return a value of type `T` based on a set of /// Interface for classes that can return a value of type `T` based on a set of
......
...@@ -18,6 +18,7 @@ import 'feedback.dart'; ...@@ -18,6 +18,7 @@ import 'feedback.dart';
import 'input_decorator.dart'; import 'input_decorator.dart';
import 'material.dart'; import 'material.dart';
import 'material_localizations.dart'; import 'material_localizations.dart';
import 'material_state.dart';
import 'selectable_text.dart' show iOSHorizontalOffset; import 'selectable_text.dart' show iOSHorizontalOffset;
import 'text_selection.dart'; import 'text_selection.dart';
import 'theme.dart'; import 'theme.dart';
...@@ -346,6 +347,7 @@ class TextField extends StatefulWidget { ...@@ -346,6 +347,7 @@ class TextField extends StatefulWidget {
this.dragStartBehavior = DragStartBehavior.start, this.dragStartBehavior = DragStartBehavior.start,
this.enableInteractiveSelection = true, this.enableInteractiveSelection = true,
this.onTap, this.onTap,
this.mouseCursor,
this.buildCounter, this.buildCounter,
this.scrollController, this.scrollController,
this.scrollPhysics, this.scrollPhysics,
...@@ -685,6 +687,25 @@ class TextField extends StatefulWidget { ...@@ -685,6 +687,25 @@ class TextField extends StatefulWidget {
/// {@endtemplate} /// {@endtemplate}
final GestureTapCallback onTap; final GestureTapCallback onTap;
/// 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.error].
/// * [MaterialState.hovered].
/// * [MaterialState.focused].
/// * [MaterialState.disabled].
///
/// If this property is null, [MaterialStateMouseCursor.textable] will be used.
///
/// The [mouseCursor] is the only property of [TextField] that controls the
/// appearance of the mouse pointer. All other properties related to "cursor"
/// stand for the text cursor, which is usually a blinking vertical line at
/// the editing position.
final MouseCursor mouseCursor;
/// Callback that generates a custom [InputDecorator.counter] widget. /// Callback that generates a custom [InputDecorator.counter] widget.
/// ///
/// See [InputCounterWidgetBuilder] for an explanation of the passed in /// See [InputCounterWidgetBuilder] for an explanation of the passed in
...@@ -799,6 +820,10 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe ...@@ -799,6 +820,10 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe
int get _currentLength => _effectiveController.value.text.runes.length; int get _currentLength => _effectiveController.value.text.runes.length;
bool get _hasIntrinsicError => widget.maxLength != null && widget.maxLength > 0 && _effectiveController.value.text.runes.length > widget.maxLength;
bool get _hasError => widget.decoration?.errorText != null || _hasIntrinsicError;
InputDecoration _getEffectiveDecoration() { InputDecoration _getEffectiveDecoration() {
final MaterialLocalizations localizations = MaterialLocalizations.of(context); final MaterialLocalizations localizations = MaterialLocalizations.of(context);
final ThemeData themeData = Theme.of(context); final ThemeData themeData = Theme.of(context);
...@@ -849,9 +874,9 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe ...@@ -849,9 +874,9 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe
counterText += '/${widget.maxLength}'; counterText += '/${widget.maxLength}';
final int remaining = (widget.maxLength - currentLength).clamp(0, widget.maxLength) as int; final int remaining = (widget.maxLength - currentLength).clamp(0, widget.maxLength) as int;
semanticCounterText = localizations.remainingTextFieldCharacterCount(remaining); semanticCounterText = localizations.remainingTextFieldCharacterCount(remaining);
}
// Handle length exceeds maxLength if (_hasIntrinsicError) {
if (_effectiveController.value.text.runes.length > widget.maxLength) {
return effectiveDecoration.copyWith( return effectiveDecoration.copyWith(
errorText: effectiveDecoration.errorText ?? '', errorText: effectiveDecoration.errorText ?? '',
counterStyle: effectiveDecoration.errorStyle counterStyle: effectiveDecoration.errorStyle
...@@ -860,7 +885,6 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe ...@@ -860,7 +885,6 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe
semanticCounterText: semanticCounterText, semanticCounterText: semanticCounterText,
); );
} }
}
return effectiveDecoration.copyWith( return effectiveDecoration.copyWith(
counterText: counterText, counterText: counterText,
...@@ -1113,10 +1137,20 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe ...@@ -1113,10 +1137,20 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe
child: child, child: child,
); );
} }
final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor>(
widget.mouseCursor ?? MaterialStateMouseCursor.textable,
<MaterialState>{
if (!_isEnabled) MaterialState.disabled,
if (_isHovering) MaterialState.hovered,
if (focusNode.hasFocus) MaterialState.focused,
if (_hasError) MaterialState.error,
},
);
return IgnorePointer( return IgnorePointer(
ignoring: !_isEnabled, ignoring: !_isEnabled,
child: MouseRegion( child: MouseRegion(
cursor: SystemMouseCursors.text, cursor: effectiveMouseCursor,
onEnter: (PointerEnterEvent event) => _handleHover(true), onEnter: (PointerEnterEvent event) => _handleHover(true),
onExit: (PointerExitEvent event) => _handleHover(false), onExit: (PointerExitEvent event) => _handleHover(false),
child: AnimatedBuilder( child: AnimatedBuilder(
......
...@@ -995,8 +995,9 @@ class EditableText extends StatefulWidget { ...@@ -995,8 +995,9 @@ class EditableText extends StatefulWidget {
/// If this property is null, [SystemMouseCursors.text] will be used. /// If this property is null, [SystemMouseCursors.text] will be used.
/// ///
/// The [mouseCursor] is the only property of [EditableText] that controls the /// The [mouseCursor] is the only property of [EditableText] that controls the
/// mouse pointer. All other properties related to "cursor" stands for the text /// appearance of the mouse pointer. All other properties related to "cursor"
/// cursor, which is usually a blinking vertical line at the editing position. /// stands for the text cursor, which is usually a blinking vertical line at
/// the editing position.
final MouseCursor mouseCursor; final MouseCursor mouseCursor;
/// If true, the [RenderEditable] created by this widget will not handle /// If true, the [RenderEditable] created by this widget will not handle
......
...@@ -7941,7 +7941,10 @@ void main() { ...@@ -7941,7 +7941,10 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
const MaterialApp( const MaterialApp(
home: Material( home: Material(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: TextField( child: TextField(
mouseCursor: SystemMouseCursors.grab,
decoration: InputDecoration( decoration: InputDecoration(
// Add an icon so that the left edge is not the text area // Add an icon so that the left edge is not the text area
icon: Icon(Icons.person), icon: Icon(Icons.person),
...@@ -7949,18 +7952,63 @@ void main() { ...@@ -7949,18 +7952,63 @@ void main() {
), ),
), ),
), ),
),
); );
// Center, which is within the text area
final Offset center = tester.getCenter(find.byType(TextField));
// Top left, which is not the text area
final Offset edge = tester.getTopLeft(find.byType(TextField)) + const Offset(1, 1);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
await gesture.addPointer(location: tester.getCenter(find.byType(TextField))); await gesture.addPointer(location: center);
addTearDown(gesture.removePointer); addTearDown(gesture.removePointer);
await tester.pump(); await tester.pump();
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text); expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.grab);
// Test default cursor
await tester.pumpWidget(
const MaterialApp(
home: Material(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: TextField(
decoration: InputDecoration(
icon: Icon(Icons.person),
),
),
),
),
),
);
// Test top left, which is not the text area
await gesture.moveTo(tester.getTopLeft(find.byType(TextField)) + const Offset(1, 1));
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text); expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
await gesture.moveTo(edge);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
await gesture.moveTo(center);
// Test default cursor when disabled
await tester.pumpWidget(
const MaterialApp(
home: Material(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: TextField(
enabled: false,
decoration: InputDecoration(
icon: Icon(Icons.person),
),
),
),
),
),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
await gesture.moveTo(edge);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
await gesture.moveTo(center);
}); });
} }
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