Unverified Commit 4102486d authored by Gary Qian's avatar Gary Qian Committed by GitHub

Center iOS caret, remove constant offsets that do not scale (#31687)

parent 785df279
dadf2c9b8a8de8fc605e094a539410d83f700b34
5e0cf0b9b347526d299718a4755e2d65cffacaba
......@@ -1501,9 +1501,9 @@ class RenderEditable extends RenderBox {
Rect get _getCaretPrototype {
switch(defaultTargetPlatform){
case TargetPlatform.iOS:
return Rect.fromLTWH(0.0, -_kCaretHeightOffset + .5, cursorWidth, preferredLineHeight + 2);
return Rect.fromLTWH(0.0, 0.0, cursorWidth, preferredLineHeight + 2);
default:
return Rect.fromLTWH(0.0, _kCaretHeightOffset, cursorWidth, preferredLineHeight - 2.0 * _kCaretHeightOffset);
return Rect.fromLTWH(0.0, 0.0, cursorWidth, preferredLineHeight - 2.0 * _kCaretHeightOffset);
}
}
@override
......@@ -1550,19 +1550,32 @@ class RenderEditable extends RenderBox {
if (_cursorOffset != null)
caretRect = caretRect.shift(_cursorOffset);
// Override the height to take the full height of the glyph at the TextPosition
// when not on iOS. iOS has special handling that creates a taller caret.
// TODO(garyq): See the TODO for _getCaretPrototype.
if (defaultTargetPlatform != TargetPlatform.iOS && _textPainter.getFullHeightForCaret(textPosition, _caretPrototype) != null) {
caretRect = Rect.fromLTWH(
caretRect.left,
// Offset by _kCaretHeightOffset to counteract the same value added in
// _getCaretPrototype. This prevents this from scaling poorly for small
// font sizes.
caretRect.top - _kCaretHeightOffset,
caretRect.width,
_textPainter.getFullHeightForCaret(textPosition, _caretPrototype),
);
if (_textPainter.getFullHeightForCaret(textPosition, _caretPrototype) != null) {
switch (defaultTargetPlatform) {
case TargetPlatform.iOS: {
final double heightDiff = _textPainter.getFullHeightForCaret(textPosition, _caretPrototype) - caretRect.height;
// Center the caret vertically along the text.
caretRect = Rect.fromLTWH(
caretRect.left,
caretRect.top + heightDiff / 2,
caretRect.width,
caretRect.height,
);
break;
}
default: {
// Override the height to take the full height of the glyph at the TextPosition
// when not on iOS. iOS has special handling that creates a taller caret.
// TODO(garyq): See the TODO for _getCaretPrototype.
caretRect = Rect.fromLTWH(
caretRect.left,
caretRect.top,
caretRect.width,
_textPainter.getFullHeightForCaret(textPosition, _caretPrototype),
);
break;
}
}
}
caretRect = caretRect.shift(_getPixelPerfectCursorOffset(caretRect));
......
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/rendering.dart';
......@@ -350,6 +351,61 @@ void main() {
debugDefaultTargetPlatformOverride = null;
});
testWidgets('cursor android golden', (WidgetTester tester) async {
final Widget widget = CupertinoApp(
home: Center(
child: RepaintBoundary(
key: const ValueKey<int>(1),
child: ConstrainedBox(
constraints: BoxConstraints.loose(const Size(400, 400)),
child: const CupertinoTextField(),
),
),
),
);
await tester.pumpWidget(widget);
const String testValue = 'A short phrase';
await tester.enterText(find.byType(CupertinoTextField), testValue);
await tester.tapAt(textOffsetToPosition(tester, testValue.length));
await tester.pump();
await expectLater(
find.byKey(const ValueKey<int>(1)),
matchesGoldenFile('text_field_cursor_test.0.1.png'),
);
}, skip: !Platform.isLinux);
testWidgets('cursor iOS golden', (WidgetTester tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
final Widget widget = CupertinoApp(
home: Center(
child: RepaintBoundary(
key: const ValueKey<int>(1),
child: ConstrainedBox(
constraints: BoxConstraints.loose(const Size(400, 400)),
child: const CupertinoTextField(),
),
),
),
);
await tester.pumpWidget(widget);
const String testValue = 'A short phrase';
await tester.enterText(find.byType(CupertinoTextField), testValue);
await tester.tapAt(textOffsetToPosition(tester, testValue.length));
await tester.pump();
debugDefaultTargetPlatformOverride = null;
await expectLater(
find.byKey(const ValueKey<int>(1)),
matchesGoldenFile('text_field_cursor_test.1.1.png'),
);
}, skip: !Platform.isLinux);
testWidgets(
'can control text content via controller',
(WidgetTester tester) async {
......
......@@ -381,6 +381,61 @@ void main() {
expect(textField.cursorRadius, const Radius.circular(3.0));
});
testWidgets('cursor android golden', (WidgetTester tester) async {
final Widget widget = overlay(
child: const RepaintBoundary(
key: ValueKey<int>(1),
child: TextField(
cursorColor: Colors.blue,
cursorWidth: 15,
cursorRadius: Radius.circular(3.0),
),
),
);
await tester.pumpWidget(widget);
const String testValue = 'A short phrase';
await tester.enterText(find.byType(TextField), testValue);
await skipPastScrollingAnimation(tester);
await tester.tapAt(textOffsetToPosition(tester, testValue.length));
await tester.pump();
await expectLater(
find.byKey(const ValueKey<int>(1)),
matchesGoldenFile('text_field_cursor_test.0.0.png'),
);
}, skip: !Platform.isLinux);
testWidgets('cursor iOS golden', (WidgetTester tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
final Widget widget = overlay(
child: const RepaintBoundary(
key: ValueKey<int>(1),
child: TextField(
cursorColor: Colors.blue,
cursorWidth: 15,
cursorRadius: Radius.circular(3.0),
),
),
);
await tester.pumpWidget(widget);
const String testValue = 'A short phrase';
await tester.enterText(find.byType(TextField), testValue);
await skipPastScrollingAnimation(tester);
await tester.tapAt(textOffsetToPosition(tester, testValue.length));
await tester.pump();
debugDefaultTargetPlatformOverride = null;
await expectLater(
find.byKey(const ValueKey<int>(1)),
matchesGoldenFile('text_field_cursor_test.1.0.png'),
);
}, skip: !Platform.isLinux);
testWidgets('text field selection toolbar renders correctly inside opacity', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
......
......@@ -4,6 +4,7 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
......@@ -575,6 +576,8 @@ void main() {
});
testWidgets('Floating cursor is painted on iOS', (WidgetTester tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
final TextEditingController controller = TextEditingController();
const TextStyle textStyle = TextStyle();
const String text = 'hello world this is fun and cool and awesome!';
......@@ -617,14 +620,14 @@ void main() {
expect(editable, paints
..rrect(
rrect: RRect.fromRectAndRadius(
const Rect.fromLTRB(463.3333435058594, 0.0833332538604736, 465.3333435058594, 16.083333015441895),
const Rect.fromLTRB(463.3333435058594, -0.916666666666668, 465.3333435058594, 17.083333015441895),
const Radius.circular(2.0),
),
color: const Color(0xff8e8e93),
)
..rrect(
rrect: RRect.fromRectAndRadius(
const Rect.fromLTRB(463.8333435058594, 1.0833336114883423, 466.8333435058594, 15.083333969116211),
const Rect.fromLTRB(463.8333435058594, -0.916666666666668, 466.8333435058594, 19.083333969116211),
const Radius.circular(1.0),
),
color: const Color(0xbf2196f3),
......@@ -642,14 +645,14 @@ void main() {
expect(find.byType(EditableText), paints
..rrect(
rrect: RRect.fromRectAndRadius(
const Rect.fromLTRB(191.3333282470703, 0.0833332538604736, 193.3333282470703, 16.083333015441895),
const Rect.fromLTRB(191.3333282470703, -0.916666666666668, 193.3333282470703, 17.083333015441895),
const Radius.circular(2.0),
),
color: const Color(0xff8e8e93),
)
..rrect(
rrect: RRect.fromRectAndRadius(
const Rect.fromLTRB(193.83334350585938, 1.0833336114883423, 196.83334350585938, 15.083333969116211),
const Rect.fromLTRB(193.83334350585938, -0.916666666666668, 196.83334350585938, 19.083333969116211),
const Radius.circular(1.0)),
color: const Color(0xbf2196f3),
),
......@@ -657,5 +660,70 @@ void main() {
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.End));
await tester.pumpAndSettle();
debugDefaultTargetPlatformOverride = null;
});
testWidgets('cursor layout iOS', (WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>();
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
String changedValue;
final Widget widget = MaterialApp(
theme: ThemeData(platform: TargetPlatform.iOS),
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(platform: TargetPlatform.iOS).black.subhead,
cursorColor: Colors.blue,
selectionControls: materialTextSelectionControls,
keyboardType: TextInputType.text,
onChanged: (String value) {
changedValue = value;
},
cursorWidth: 15.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.pump();
await tester.pump(const Duration(milliseconds: 500));
await tester.pump(const Duration(milliseconds: 500));
await tester.tap(find.text('PASTE'));
await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
await tester.pump(const Duration(milliseconds: 500));
expect(changedValue, clipboardContent);
await expectLater(
find.byKey(const ValueKey<int>(1)),
matchesGoldenFile('editable_text_test.2.0.png'),
);
debugDefaultTargetPlatformOverride = null;
}, skip: !Platform.isLinux);
}
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