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 {
///
/// This should only be used as parameters when they are documented to take
/// [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> {
/// Creates a [MaterialStateMouseCursor].
const MaterialStateMouseCursor();
......@@ -257,22 +265,47 @@ abstract class MaterialStateMouseCursor extends MouseCursor implements MaterialS
/// disabled, the cursor resolves to [SystemMouseCursors.basic].
///
/// 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 {
const _ClickableMouseCursor();
class _EnabledAndDisabledMouseCursor extends MaterialStateMouseCursor {
const _EnabledAndDisabledMouseCursor({
this.enabledCursor,
this.disabledCursor,
this.name,
});
final MouseCursor enabledCursor;
final MouseCursor disabledCursor;
final String name;
@override
MouseCursor resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return SystemMouseCursors.basic;
return disabledCursor;
}
return SystemMouseCursors.click;
return enabledCursor;
}
@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
......
......@@ -18,6 +18,7 @@ import 'feedback.dart';
import 'input_decorator.dart';
import 'material.dart';
import 'material_localizations.dart';
import 'material_state.dart';
import 'selectable_text.dart' show iOSHorizontalOffset;
import 'text_selection.dart';
import 'theme.dart';
......@@ -346,6 +347,7 @@ class TextField extends StatefulWidget {
this.dragStartBehavior = DragStartBehavior.start,
this.enableInteractiveSelection = true,
this.onTap,
this.mouseCursor,
this.buildCounter,
this.scrollController,
this.scrollPhysics,
......@@ -685,6 +687,25 @@ class TextField extends StatefulWidget {
/// {@endtemplate}
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.
///
/// See [InputCounterWidgetBuilder] for an explanation of the passed in
......@@ -799,6 +820,10 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe
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() {
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
final ThemeData themeData = Theme.of(context);
......@@ -849,17 +874,16 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe
counterText += '/${widget.maxLength}';
final int remaining = (widget.maxLength - currentLength).clamp(0, widget.maxLength) as int;
semanticCounterText = localizations.remainingTextFieldCharacterCount(remaining);
}
// Handle length exceeds maxLength
if (_effectiveController.value.text.runes.length > widget.maxLength) {
return effectiveDecoration.copyWith(
errorText: effectiveDecoration.errorText ?? '',
counterStyle: effectiveDecoration.errorStyle
?? themeData.textTheme.caption.copyWith(color: themeData.errorColor),
counterText: counterText,
semanticCounterText: semanticCounterText,
);
}
if (_hasIntrinsicError) {
return effectiveDecoration.copyWith(
errorText: effectiveDecoration.errorText ?? '',
counterStyle: effectiveDecoration.errorStyle
?? themeData.textTheme.caption.copyWith(color: themeData.errorColor),
counterText: counterText,
semanticCounterText: semanticCounterText,
);
}
return effectiveDecoration.copyWith(
......@@ -1113,10 +1137,20 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe
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(
ignoring: !_isEnabled,
child: MouseRegion(
cursor: SystemMouseCursors.text,
cursor: effectiveMouseCursor,
onEnter: (PointerEnterEvent event) => _handleHover(true),
onExit: (PointerExitEvent event) => _handleHover(false),
child: AnimatedBuilder(
......
......@@ -995,8 +995,9 @@ class EditableText extends StatefulWidget {
/// If this property is null, [SystemMouseCursors.text] will be used.
///
/// The [mouseCursor] is the only property of [EditableText] that controls the
/// mouse pointer. All other properties related to "cursor" stands for the text
/// cursor, which is usually a blinking vertical line at the editing position.
/// appearance of the mouse pointer. All other properties related to "cursor"
/// stands for the text cursor, which is usually a blinking vertical line at
/// the editing position.
final MouseCursor mouseCursor;
/// If true, the [RenderEditable] created by this widget will not handle
......
......@@ -7941,26 +7941,74 @@ void main() {
await tester.pumpWidget(
const MaterialApp(
home: Material(
child: TextField(
decoration: InputDecoration(
// Add an icon so that the left edge is not the text area
icon: Icon(Icons.person),
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: TextField(
mouseCursor: SystemMouseCursors.grab,
decoration: InputDecoration(
// Add an icon so that the left edge is not the text area
icon: Icon(Icons.person),
),
),
),
),
),
);
// 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);
await gesture.addPointer(location: tester.getCenter(find.byType(TextField)));
await gesture.addPointer(location: center);
addTearDown(gesture.removePointer);
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);
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