Unverified Commit 5d854f63 authored by Pierre-Louis's avatar Pierre-Louis Committed by GitHub

Implement customizable cursor height (#61714)

parent 3e7fe378
......@@ -260,6 +260,7 @@ class CupertinoTextField extends StatefulWidget {
this.inputFormatters,
this.enabled,
this.cursorWidth = 2.0,
this.cursorHeight,
this.cursorRadius = const Radius.circular(2.0),
this.cursorColor,
this.selectionHeightStyle = ui.BoxHeightStyle.tight,
......@@ -544,6 +545,9 @@ class CupertinoTextField extends StatefulWidget {
/// {@macro flutter.widgets.editableText.cursorWidth}
final double cursorWidth;
/// {@macro flutter.widgets.editableText.cursorHeight}
final double cursorHeight;
/// {@macro flutter.widgets.editableText.cursorRadius}
final Radius cursorRadius;
......@@ -625,6 +629,9 @@ class CupertinoTextField extends StatefulWidget {
properties.add(DiagnosticsProperty<bool>('expands', expands, defaultValue: false));
properties.add(IntProperty('maxLength', maxLength, defaultValue: null));
properties.add(FlagProperty('maxLengthEnforced', value: maxLengthEnforced, ifTrue: 'max length enforced'));
properties.add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0));
properties.add(DoubleProperty('cursorHeight', cursorHeight, defaultValue: null));
properties.add(DiagnosticsProperty<Radius>('cursorRadius', cursorRadius, defaultValue: null));
properties.add(createCupertinoColorProperty('cursorColor', cursorColor, defaultValue: null));
properties.add(FlagProperty('selectionEnabled', value: selectionEnabled, defaultValue: true, ifFalse: 'selection disabled'));
properties.add(DiagnosticsProperty<ScrollController>('scrollController', scrollController, defaultValue: null));
......@@ -954,6 +961,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
inputFormatters: formatters,
rendererIgnoresPointer: true,
cursorWidth: widget.cursorWidth,
cursorHeight: widget.cursorHeight,
cursorRadius: widget.cursorRadius,
cursorColor: cursorColor,
cursorOpacityAnimates: true,
......
......@@ -217,6 +217,7 @@ class SelectableText extends StatefulWidget {
this.minLines,
this.maxLines,
this.cursorWidth = 2.0,
this.cursorHeight,
this.cursorRadius,
this.cursorColor,
this.dragStartBehavior = DragStartBehavior.start,
......@@ -267,6 +268,7 @@ class SelectableText extends StatefulWidget {
this.minLines,
this.maxLines,
this.cursorWidth = 2.0,
this.cursorHeight,
this.cursorRadius,
this.cursorColor,
this.dragStartBehavior = DragStartBehavior.start,
......@@ -364,6 +366,9 @@ class SelectableText extends StatefulWidget {
/// {@macro flutter.widgets.editableText.cursorWidth}
final double cursorWidth;
/// {@macro flutter.widgets.editableText.cursorHeight}
final double cursorHeight;
/// {@macro flutter.widgets.editableText.cursorRadius}
final Radius cursorRadius;
......@@ -433,6 +438,7 @@ class SelectableText extends StatefulWidget {
properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
properties.add(DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: null));
properties.add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0));
properties.add(DoubleProperty('cursorHeight', cursorHeight, defaultValue: null));
properties.add(DiagnosticsProperty<Radius>('cursorRadius', cursorRadius, defaultValue: null));
properties.add(DiagnosticsProperty<Color>('cursorColor', cursorColor, defaultValue: null));
properties.add(FlagProperty('selectionEnabled', value: selectionEnabled, defaultValue: true, ifFalse: 'selection disabled'));
......@@ -626,6 +632,7 @@ class _SelectableTextState extends State<SelectableText> with AutomaticKeepAlive
onSelectionHandleTapped: _handleSelectionHandleTapped,
rendererIgnoresPointer: true,
cursorWidth: widget.cursorWidth,
cursorHeight: widget.cursorHeight,
cursorRadius: cursorRadius,
cursorColor: cursorColor,
cursorOpacityAnimates: cursorOpacityAnimates,
......
......@@ -339,6 +339,7 @@ class TextField extends StatefulWidget {
this.inputFormatters,
this.enabled,
this.cursorWidth = 2.0,
this.cursorHeight,
this.cursorRadius,
this.cursorColor,
this.selectionHeightStyle = ui.BoxHeightStyle.tight,
......@@ -628,6 +629,9 @@ class TextField extends StatefulWidget {
/// {@macro flutter.widgets.editableText.cursorWidth}
final double cursorWidth;
/// {@macro flutter.widgets.editableText.cursorHeight}
final double cursorHeight;
/// {@macro flutter.widgets.editableText.cursorRadius}
final Radius cursorRadius;
......@@ -779,6 +783,7 @@ class TextField extends StatefulWidget {
properties.add(DiagnosticsProperty<TextAlignVertical>('textAlignVertical', textAlignVertical, defaultValue: null));
properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
properties.add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0));
properties.add(DoubleProperty('cursorHeight', cursorHeight, defaultValue: null));
properties.add(DiagnosticsProperty<Radius>('cursorRadius', cursorRadius, defaultValue: null));
properties.add(ColorProperty('cursorColor', cursorColor, defaultValue: null));
properties.add(DiagnosticsProperty<Brightness>('keyboardAppearance', keyboardAppearance, defaultValue: null));
......@@ -1100,6 +1105,7 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe
rendererIgnoresPointer: true,
mouseCursor: MouseCursor.defer, // TextField will handle the cursor
cursorWidth: widget.cursorWidth,
cursorHeight: widget.cursorHeight,
cursorRadius: cursorRadius,
cursorColor: cursorColor,
selectionHeightStyle: widget.selectionHeightStyle,
......
......@@ -172,6 +172,7 @@ class TextFormField extends FormField<String> {
List<TextInputFormatter> inputFormatters,
bool enabled,
double cursorWidth = 2.0,
double cursorHeight,
Radius cursorRadius,
Color cursorColor,
Brightness keyboardAppearance,
......@@ -264,6 +265,7 @@ class TextFormField extends FormField<String> {
inputFormatters: inputFormatters,
enabled: enabled ?? decoration?.enabled ?? true,
cursorWidth: cursorWidth,
cursorHeight: cursorHeight,
cursorRadius: cursorRadius,
cursorColor: cursorColor,
scrollPadding: scrollPadding,
......
......@@ -209,6 +209,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
bool obscureText = false,
Locale locale,
double cursorWidth = 1.0,
double cursorHeight,
Radius cursorRadius,
bool paintCursorAboveText = false,
Offset cursorOffset,
......@@ -245,6 +246,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
assert(obscureText != null),
assert(textSelectionDelegate != null),
assert(cursorWidth != null && cursorWidth >= 0.0),
assert(cursorHeight == null || cursorHeight >= 0.0),
assert(readOnly != null),
assert(forceLine != null),
assert(devicePixelRatio != null),
......@@ -271,6 +273,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
_selection = selection,
_offset = offset,
_cursorWidth = cursorWidth,
_cursorHeight = cursorHeight,
_cursorRadius = cursorRadius,
_paintCursorOnTop = paintCursorAboveText,
_cursorOffset = cursorOffset,
......@@ -1107,6 +1110,16 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
markNeedsLayout();
}
/// How tall the cursor will be.
double get cursorHeight => _cursorHeight ?? preferredLineHeight;
double _cursorHeight;
set cursorHeight(double value) {
if (_cursorHeight == value)
return;
_cursorHeight = value;
markNeedsLayout();
}
/// {@template flutter.rendering.editable.paintCursorOnTop}
/// If the cursor should be painted on top of the text or underneath it.
///
......@@ -1563,7 +1576,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
final Offset caretOffset = _textPainter.getOffsetForCaret(caretPosition, _caretPrototype);
// This rect is the same as _caretPrototype but without the vertical padding.
Rect rect = Rect.fromLTWH(0.0, 0.0, cursorWidth, preferredLineHeight).shift(caretOffset + _paintOffset);
Rect rect = Rect.fromLTWH(0.0, 0.0, cursorWidth, cursorHeight).shift(caretOffset + _paintOffset);
// Add additional cursor offset (generally only if on iOS).
if (_cursorOffset != null)
rect = rect.shift(_cursorOffset);
......@@ -1887,12 +1900,12 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
return Rect.fromLTWH(0.0, 0.0, cursorWidth, preferredLineHeight + 2);
return Rect.fromLTWH(0.0, 0.0, cursorWidth, cursorHeight + 2);
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
return Rect.fromLTWH(0.0, _kCaretHeightOffset, cursorWidth, preferredLineHeight - 2.0 * _kCaretHeightOffset);
return Rect.fromLTWH(0.0, _kCaretHeightOffset, cursorWidth, cursorHeight - 2.0 * _kCaretHeightOffset);
}
return null;
}
......
......@@ -419,6 +419,7 @@ class EditableText extends StatefulWidget {
this.mouseCursor,
this.rendererIgnoresPointer = false,
this.cursorWidth = 2.0,
this.cursorHeight,
this.cursorRadius,
this.cursorOpacityAnimates = false,
this.cursorOffset,
......@@ -1050,6 +1051,13 @@ class EditableText extends StatefulWidget {
/// {@endtemplate}
final double cursorWidth;
/// {@template flutter.widgets.editableText.cursorHeight}
/// How tall the cursor will be.
///
/// If this property is null, [RenderEditable.preferredLineHeight] will be used.
/// {@endtemplate}
final double cursorHeight;
/// {@template flutter.widgets.editableText.cursorRadius}
/// How rounded the corners of the cursor should be.
///
......@@ -2297,6 +2305,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
onCaretChanged: _handleCaretChanged,
rendererIgnoresPointer: widget.rendererIgnoresPointer,
cursorWidth: widget.cursorWidth,
cursorHeight: widget.cursorHeight,
cursorRadius: widget.cursorRadius,
cursorOffset: widget.cursorOffset,
selectionHeightStyle: widget.selectionHeightStyle,
......@@ -2379,6 +2388,7 @@ class _Editable extends LeafRenderObjectWidget {
this.onCaretChanged,
this.rendererIgnoresPointer = false,
this.cursorWidth,
this.cursorHeight,
this.cursorRadius,
this.cursorOffset,
this.paintCursorAboveText,
......@@ -2426,6 +2436,7 @@ class _Editable extends LeafRenderObjectWidget {
final CaretChangedHandler onCaretChanged;
final bool rendererIgnoresPointer;
final double cursorWidth;
final double cursorHeight;
final Radius cursorRadius;
final Offset cursorOffset;
final bool paintCursorAboveText;
......@@ -2469,6 +2480,7 @@ class _Editable extends LeafRenderObjectWidget {
textHeightBehavior: textHeightBehavior,
textWidthBasis: textWidthBasis,
cursorWidth: cursorWidth,
cursorHeight: cursorHeight,
cursorRadius: cursorRadius,
cursorOffset: cursorOffset,
paintCursorAboveText: paintCursorAboveText,
......@@ -2513,6 +2525,7 @@ class _Editable extends LeafRenderObjectWidget {
..obscuringCharacter = obscuringCharacter
..obscureText = obscureText
..cursorWidth = cursorWidth
..cursorHeight = cursorHeight
..cursorRadius = cursorRadius
..cursorOffset = cursorOffset
..selectionHeightStyle = selectionHeightStyle
......
......@@ -334,6 +334,7 @@ void main() {
final TextField textField = tester.firstWidget(find.byType(TextField));
expect(textField.cursorWidth, 2.0);
expect(textField.cursorHeight, null);
expect(textField.cursorRadius, null);
});
......@@ -686,6 +687,28 @@ void main() {
EditableText.debugDeterministicCursor = false;
});
testWidgets('cursor layout has correct height', (WidgetTester tester) async {
EditableText.debugDeterministicCursor = true;
await tester.pumpWidget(
overlay(
child: const RepaintBoundary(
child: TextField(
cursorWidth: 15.0,
cursorHeight: 30.0,
),
),
)
);
await tester.enterText(find.byType(TextField), ' ');
await skipPastScrollingAnimation(tester);
await expectLater(
find.byType(TextField),
matchesGoldenFile('text_field_cursor_width_test.2.png'),
);
EditableText.debugDeterministicCursor = false;
});
testWidgets('Overflowing a line with spaces stops the cursor at the end', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
......@@ -7093,6 +7116,7 @@ void main() {
smartQuotesType: SmartQuotesType.disabled,
enabled: false,
cursorWidth: 1.0,
cursorHeight: 1.0,
cursorRadius: Radius.zero,
cursorColor: Color(0xff00ff00),
keyboardAppearance: Brightness.dark,
......@@ -7120,6 +7144,7 @@ void main() {
'textAlign: end',
'textDirection: ltr',
'cursorWidth: 1.0',
'cursorHeight: 1.0',
'cursorRadius: Radius.circular(0.0)',
'cursorColor: Color(0xff00ff00)',
'keyboardAppearance: Brightness.dark',
......
......@@ -122,6 +122,7 @@ void main() {
testWidgets('Passes cursor attributes to underlying TextField', (WidgetTester tester) async {
const double cursorWidth = 3.14;
const double cursorHeight = 6.28;
const Radius cursorRadius = Radius.circular(4);
const Color cursorColor = Colors.purple;
......@@ -131,6 +132,7 @@ void main() {
child: Center(
child: TextFormField(
cursorWidth: cursorWidth,
cursorHeight: cursorHeight,
cursorRadius: cursorRadius,
cursorColor: cursorColor,
),
......@@ -144,6 +146,7 @@ void main() {
final TextField textFieldWidget = tester.widget(textFieldFinder);
expect(textFieldWidget.cursorWidth, cursorWidth);
expect(textFieldWidget.cursorHeight, cursorHeight);
expect(textFieldWidget.cursorRadius, cursorRadius);
expect(textFieldWidget.cursorColor, cursorColor);
});
......
......@@ -27,7 +27,7 @@ void main() {
await Clipboard.setData(const ClipboardData(text: 'Clipboard data'));
});
testWidgets('cursor has expected width and radius', (WidgetTester tester) async {
testWidgets('cursor has expected width, height, and radius', (WidgetTester tester) async {
await tester.pumpWidget(
MediaQuery(data: const MediaQueryData(devicePixelRatio: 1.0),
child: Directionality(
......@@ -39,11 +39,13 @@ void main() {
style: textStyle,
cursorColor: cursorColor,
cursorWidth: 10.0,
cursorHeight: 10.0,
cursorRadius: const Radius.circular(2.0),
))));
final EditableText editableText = tester.firstWidget(find.byType(EditableText));
expect(editableText.cursorWidth, 10.0);
expect(editableText.cursorHeight, 10.0);
expect(editableText.cursorRadius.x, 2.0);
});
......@@ -84,14 +86,11 @@ void main() {
final Finder textFinder = find.byKey(editableTextKey);
await tester.longPress(textFinder);
tester.state<EditableTextState>(textFinder).showToolbar();
await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
await tester.pump(const Duration(milliseconds: 500));
await tester.pumpAndSettle();
await tester.tap(find.text('Paste'));
await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
await tester.pump(const Duration(milliseconds: 500));
// Wait for cursor to appear.
await tester.pump(const Duration(milliseconds: 600));
expect(changedValue, clipboardContent);
......@@ -775,14 +774,11 @@ void main() {
final Finder textFinder = find.byKey(editableTextKey);
await tester.longPress(textFinder);
tester.state<EditableTextState>(textFinder).showToolbar();
await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
await tester.pump(const Duration(milliseconds: 500));
await tester.pumpAndSettle();
await tester.tap(find.text('Paste'));
await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
await tester.pump(const Duration(milliseconds: 500));
// Wait for cursor to appear.
await tester.pump(const Duration(milliseconds: 600));
expect(changedValue, clipboardContent);
......@@ -791,4 +787,61 @@ void main() {
matchesGoldenFile('editable_text_test.2.png'),
);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('cursor layout has correct height', (WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>();
String changedValue;
final Widget widget = MaterialApp(
home: RepaintBoundary(
key: const ValueKey<int>(1),
child: Column(
children: <Widget>[
const SizedBox(width: 10, height: 10),
EditableText(
backgroundCursorColor: Colors.grey,
key: editableTextKey,
controller: TextEditingController(),
focusNode: FocusNode(),
style: Typography.material2018(platform: TargetPlatform.iOS).black.subtitle1,
cursorColor: Colors.blue,
selectionControls: materialTextSelectionControls,
keyboardType: TextInputType.text,
onChanged: (String value) {
changedValue = value;
},
cursorWidth: 15.0,
cursorHeight: 30.0,
),
],
),
),
);
await tester.pumpWidget(widget);
// Populate a fake clipboard.
const String clipboardContent = 'Hello world!';
SystemChannels.platform.setMockMethodCallHandler((MethodCall methodCall) async {
if (methodCall.method == 'Clipboard.getData')
return const <String, dynamic>{'text': clipboardContent};
return null;
});
// Long-press to bring up the text editing controls.
final Finder textFinder = find.byKey(editableTextKey);
await tester.longPress(textFinder);
tester.state<EditableTextState>(textFinder).showToolbar();
await tester.pumpAndSettle();
await tester.tap(find.text('Paste'));
// Wait for cursor to appear.
await tester.pump(const Duration(milliseconds: 600));
expect(changedValue, clipboardContent);
await expectLater(
find.byKey(const ValueKey<int>(1)),
matchesGoldenFile('editable_text_test.3.png'),
);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
}
......@@ -129,6 +129,7 @@ void main() {
expect(editableText.enableSuggestions, isTrue);
expect(editableText.textAlign, TextAlign.start);
expect(editableText.cursorWidth, 2.0);
expect(editableText.cursorHeight, isNull);
expect(editableText.textHeightBehavior, isNull);
});
......
......@@ -206,6 +206,7 @@ void main() {
expect(selectableText.autofocus, false);
expect(selectableText.dragStartBehavior, DragStartBehavior.start);
expect(selectableText.cursorWidth, 2.0);
expect(selectableText.cursorHeight, isNull);
expect(selectableText.enableInteractiveSelection, true);
});
......@@ -250,6 +251,7 @@ void main() {
expect(selectableText.autofocus, false);
expect(selectableText.dragStartBehavior, DragStartBehavior.start);
expect(selectableText.cursorWidth, 2.0);
expect(selectableText.cursorHeight, isNull);
expect(selectableText.enableInteractiveSelection, true);
});
......@@ -3256,6 +3258,7 @@ void main() {
minLines: 2,
maxLines: 10,
cursorWidth: 1.0,
cursorHeight: 1.0,
cursorRadius: Radius.zero,
cursorColor: Color(0xff00ff00),
scrollPhysics: ClampingScrollPhysics(),
......@@ -3277,6 +3280,7 @@ void main() {
'textDirection: ltr',
'textScaleFactor: 1.0',
'cursorWidth: 1.0',
'cursorHeight: 1.0',
'cursorRadius: Radius.circular(0.0)',
'cursorColor: Color(0xff00ff00)',
'selection disabled',
......
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