Unverified Commit bcc1dc6b authored by chunhtai's avatar chunhtai Committed by GitHub

Makes TextBoundary and its subclasses public (#110367)

parent a7f028f1
...@@ -43,6 +43,7 @@ export 'src/services/system_channels.dart'; ...@@ -43,6 +43,7 @@ export 'src/services/system_channels.dart';
export 'src/services/system_chrome.dart'; export 'src/services/system_chrome.dart';
export 'src/services/system_navigator.dart'; export 'src/services/system_navigator.dart';
export 'src/services/system_sound.dart'; export 'src/services/system_sound.dart';
export 'src/services/text_boundary.dart';
export 'src/services/text_editing.dart'; export 'src/services/text_editing.dart';
export 'src/services/text_editing_delta.dart'; export 'src/services/text_editing_delta.dart';
export 'src/services/text_formatter.dart'; export 'src/services/text_formatter.dart';
......
...@@ -1129,6 +1129,12 @@ class TextPainter { ...@@ -1129,6 +1129,12 @@ class TextPainter {
/// {@endtemplate} /// {@endtemplate}
TextRange getWordBoundary(TextPosition position) { TextRange getWordBoundary(TextPosition position) {
assert(_debugAssertTextLayoutIsValid); assert(_debugAssertTextLayoutIsValid);
// TODO(chunhtai): remove this workaround once ui.Paragraph.getWordBoundary
// can handle caret position.
// https://github.com/flutter/flutter/issues/111751.
if (position.affinity == TextAffinity.upstream) {
position = TextPosition(offset: position.offset - 1);
}
return _paragraph!.getWordBoundary(position); return _paragraph!.getWordBoundary(position);
} }
......
...@@ -2040,7 +2040,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, ...@@ -2040,7 +2040,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
final TextSelection firstWord = _getWordAtOffset(firstPosition); final TextSelection firstWord = _getWordAtOffset(firstPosition);
final TextSelection lastWord = to == null ? final TextSelection lastWord = to == null ?
firstWord : _getWordAtOffset(_textPainter.getPositionForOffset(globalToLocal(to - _paintOffset))); firstWord : _getWordAtOffset(_textPainter.getPositionForOffset(globalToLocal(to - _paintOffset)));
_setSelection( _setSelection(
TextSelection( TextSelection(
baseOffset: firstWord.base.offset, baseOffset: firstWord.base.offset,
...@@ -2071,14 +2070,28 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, ...@@ -2071,14 +2070,28 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
TextSelection _getWordAtOffset(TextPosition position) { TextSelection _getWordAtOffset(TextPosition position) {
debugAssertLayoutUpToDate(); debugAssertLayoutUpToDate();
final TextRange word = _textPainter.getWordBoundary(position);
// When long-pressing past the end of the text, we want a collapsed cursor. // When long-pressing past the end of the text, we want a collapsed cursor.
if (position.offset >= word.end) { if (position.offset >= _plainText.length) {
return TextSelection.fromPosition(position); return TextSelection.fromPosition(
TextPosition(offset: _plainText.length, affinity: TextAffinity.upstream)
);
} }
// If text is obscured, the entire sentence should be treated as one word. // If text is obscured, the entire sentence should be treated as one word.
if (obscureText) { if (obscureText) {
return TextSelection(baseOffset: 0, extentOffset: _plainText.length); return TextSelection(baseOffset: 0, extentOffset: _plainText.length);
}
final TextRange word = _textPainter.getWordBoundary(position);
final int effectiveOffset;
switch (position.affinity) {
case TextAffinity.upstream:
// upstream affinity is effectively -1 in text position.
effectiveOffset = position.offset - 1;
break;
case TextAffinity.downstream:
effectiveOffset = position.offset;
break;
}
// On iOS, select the previous word if there is a previous word, or select // On iOS, select the previous word if there is a previous word, or select
// to the end of the next word if there is a next word. Select nothing if // to the end of the next word if there is a next word. Select nothing if
// there is neither a previous word nor a next word. // there is neither a previous word nor a next word.
...@@ -2086,8 +2099,8 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, ...@@ -2086,8 +2099,8 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
// If the platform is Android and the text is read only, try to select the // If the platform is Android and the text is read only, try to select the
// previous word if there is one; otherwise, select the single whitespace at // previous word if there is one; otherwise, select the single whitespace at
// the position. // the position.
} else if (TextLayoutMetrics.isWhitespace(_plainText.codeUnitAt(position.offset)) if (TextLayoutMetrics.isWhitespace(_plainText.codeUnitAt(effectiveOffset))
&& position.offset > 0) { && effectiveOffset > 0) {
assert(defaultTargetPlatform != null); assert(defaultTargetPlatform != null);
final TextRange? previousWord = _getPreviousWord(word.start); final TextRange? previousWord = _getPreviousWord(word.start);
switch (defaultTargetPlatform) { switch (defaultTargetPlatform) {
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'dart:ui';
import 'package:characters/characters.dart' show CharacterRange;
import 'text_layout_metrics.dart';
/// An interface for retrieving the logical text boundary (left-closed-right-open)
/// at a given location in a document.
///
/// The input [TextPosition] points to a position between 2 code units (which
/// can be visually represented by the caret if the selection were to collapse
/// to that position). The [TextPosition.affinity] is used to determine which
/// code unit it points. For example, `TextPosition(i, upstream)` points to
/// code unit `i - 1` and `TextPosition(i, downstream)` points to code unit `i`.
abstract class TextBoundary {
/// A constant constructor to enable subclass override.
const TextBoundary();
/// Returns the leading text boundary at the given location.
///
/// The return value must be less or equal to the input position.
TextPosition getLeadingTextBoundaryAt(TextPosition position);
/// Returns the trailing text boundary at the given location, exclusive.
///
/// The return value must be greater or equal to the input position.
TextPosition getTrailingTextBoundaryAt(TextPosition position);
/// Gets the text boundary range that encloses the input position.
TextRange getTextBoundaryAt(TextPosition position) {
return TextRange(
start: getLeadingTextBoundaryAt(position).offset,
end: getTrailingTextBoundaryAt(position).offset,
);
}
}
/// A text boundary that uses characters as logical boundaries.
///
/// This class takes grapheme clusters into account and avoid creating
/// boundaries that generate malformed utf-16 characters.
class CharacterBoundary extends TextBoundary {
/// Creates a [CharacterBoundary] with the text.
const CharacterBoundary(this._text);
final String _text;
@override
TextPosition getLeadingTextBoundaryAt(TextPosition position) {
if (position.offset <= 0) {
return const TextPosition(offset: 0);
}
if (position.offset > _text.length ||
(position.offset == _text.length && position.affinity == TextAffinity.downstream)) {
return TextPosition(offset: _text.length, affinity: TextAffinity.upstream);
}
final int endOffset;
final int startOffset;
switch (position.affinity) {
case TextAffinity.upstream:
startOffset = math.min(position.offset - 1, _text.length);
endOffset = math.min(position.offset, _text.length);
break;
case TextAffinity.downstream:
startOffset = math.min(position.offset, _text.length);
endOffset = math.min(position.offset + 1, _text.length);
break;
}
return TextPosition(
offset: CharacterRange.at(_text, startOffset, endOffset).stringBeforeLength,
);
}
@override
TextPosition getTrailingTextBoundaryAt(TextPosition position) {
if (position.offset < 0 ||
(position.offset == 0 && position.affinity == TextAffinity.upstream)) {
return const TextPosition(offset: 0);
}
if (position.offset >= _text.length) {
return TextPosition(offset: _text.length, affinity: TextAffinity.upstream);
}
final int endOffset;
final int startOffset;
switch (position.affinity) {
case TextAffinity.upstream:
startOffset = math.min(position.offset - 1, _text.length);
endOffset = math.min(position.offset, _text.length);
break;
case TextAffinity.downstream:
startOffset = math.min(position.offset, _text.length);
endOffset = math.min(position.offset + 1, _text.length);
break;
}
final CharacterRange range = CharacterRange.at(_text, startOffset, endOffset);
return TextPosition(
offset: _text.length - range.stringAfterLength,
affinity: TextAffinity.upstream,
);
}
}
/// A text boundary that uses words as logical boundaries.
///
/// This class uses [UAX #29](https://unicode.org/reports/tr29/) defined word
/// boundaries to calculate its logical boundaries.
class WordBoundary extends TextBoundary {
/// Creates a [CharacterBoundary] with the text and layout information.
const WordBoundary(this._textLayout);
final TextLayoutMetrics _textLayout;
@override
TextPosition getLeadingTextBoundaryAt(TextPosition position) {
return TextPosition(
offset: _textLayout.getWordBoundary(position).start,
affinity: TextAffinity.downstream, // ignore: avoid_redundant_argument_values
);
}
@override
TextPosition getTrailingTextBoundaryAt(TextPosition position) {
return TextPosition(
offset: _textLayout.getWordBoundary(position).end,
affinity: TextAffinity.upstream,
);
}
}
/// A text boundary that uses line breaks as logical boundaries.
///
/// The input [TextPosition]s will be interpreted as caret locations if
/// [TextLayoutMetrics.getLineAtOffset] is text-affinity-aware.
class LineBreak extends TextBoundary {
/// Creates a [CharacterBoundary] with the text and layout information.
const LineBreak(this._textLayout);
final TextLayoutMetrics _textLayout;
@override
TextPosition getLeadingTextBoundaryAt(TextPosition position) {
return TextPosition(
offset: _textLayout.getLineAtOffset(position).start,
);
}
@override
TextPosition getTrailingTextBoundaryAt(TextPosition position) {
return TextPosition(
offset: _textLayout.getLineAtOffset(position).end,
affinity: TextAffinity.upstream,
);
}
}
/// A text boundary that uses the entire document as logical boundary.
///
/// The document boundary is unique and is a constant function of the input
/// position.
class DocumentBoundary extends TextBoundary {
/// Creates a [CharacterBoundary] with the text
const DocumentBoundary(this._text);
final String _text;
@override
TextPosition getLeadingTextBoundaryAt(TextPosition position) => const TextPosition(offset: 0);
@override
TextPosition getTrailingTextBoundaryAt(TextPosition position) {
return TextPosition(
offset: _text.length,
affinity: TextAffinity.upstream,
);
}
}
...@@ -199,7 +199,8 @@ void main() { ...@@ -199,7 +199,8 @@ void main() {
return endpoints[0].point; return endpoints[0].point;
} }
Offset textOffsetToPosition(WidgetTester tester, int offset) => textOffsetToBottomLeftPosition(tester, offset) + const Offset(0, -2); // Web has a less threshold for downstream/upstream text position.
Offset textOffsetToPosition(WidgetTester tester, int offset) => textOffsetToBottomLeftPosition(tester, offset) + const Offset(kIsWeb ? 1 : 0, -2);
setUp(() async { setUp(() async {
EditableText.debugDeterministicCursor = false; EditableText.debugDeterministicCursor = false;
...@@ -2087,6 +2088,7 @@ void main() { ...@@ -2087,6 +2088,7 @@ void main() {
await tester.pump(const Duration(milliseconds: 50)); await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(textOffsetToPosition(tester, 5)); await tester.tapAt(textOffsetToPosition(tester, 5));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(controller.value.selection, isNotNull); expect(controller.value.selection, isNotNull);
expect(controller.value.selection.baseOffset, 5); expect(controller.value.selection.baseOffset, 5);
expect(controller.value.selection.extentOffset, 6); expect(controller.value.selection.extentOffset, 6);
...@@ -3053,7 +3055,7 @@ void main() { ...@@ -3053,7 +3055,7 @@ void main() {
expect(controller.selection.extentOffset, 8); expect(controller.selection.extentOffset, 8);
// Tiny movement shouldn't cause text selection to change. // Tiny movement shouldn't cause text selection to change.
await gesture.moveTo(gPos + const Offset(4.0, 0.0)); await gesture.moveTo(gPos + const Offset(2.0, 0.0));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(selectionChangedCount, 0); expect(selectionChangedCount, 0);
......
...@@ -2172,7 +2172,7 @@ void main() { ...@@ -2172,7 +2172,7 @@ void main() {
expect(controller.selection.extentOffset, 8); expect(controller.selection.extentOffset, 8);
// Tiny movement shouldn't cause text selection to change. // Tiny movement shouldn't cause text selection to change.
await gesture.moveTo(gPos + const Offset(4.0, 0.0)); await gesture.moveTo(gPos + const Offset(2.0, 0.0));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(selectionChangedCount, 0); expect(selectionChangedCount, 0);
...@@ -3372,10 +3372,7 @@ void main() { ...@@ -3372,10 +3372,7 @@ void main() {
final Offset secondPos = textOffsetToPosition(tester, testValue.indexOf('Second')); final Offset secondPos = textOffsetToPosition(tester, testValue.indexOf('Second'));
final Offset thirdPos = textOffsetToPosition(tester, testValue.indexOf('Third')); final Offset thirdPos = textOffsetToPosition(tester, testValue.indexOf('Third'));
final Offset middleStringPos = textOffsetToPosition(tester, testValue.indexOf('irst')); final Offset middleStringPos = textOffsetToPosition(tester, testValue.indexOf('irst'));
expect(firstPos.dx, 0); expect(firstPos.dx, lessThan(middleStringPos.dx));
expect(secondPos.dx, 0);
expect(thirdPos.dx, 0);
expect(middleStringPos.dx, 34);
expect(firstPos.dx, secondPos.dx); expect(firstPos.dx, secondPos.dx);
expect(firstPos.dx, thirdPos.dx); expect(firstPos.dx, thirdPos.dx);
expect(firstPos.dy, lessThan(secondPos.dy)); expect(firstPos.dy, lessThan(secondPos.dy));
...@@ -3457,8 +3454,6 @@ void main() { ...@@ -3457,8 +3454,6 @@ void main() {
// Check that the last line of text is not displayed. // Check that the last line of text is not displayed.
final Offset firstPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First')); final Offset firstPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First'));
final Offset fourthPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('Fourth')); final Offset fourthPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('Fourth'));
expect(firstPos.dx, 0);
expect(fourthPos.dx, 0);
expect(firstPos.dx, fourthPos.dx); expect(firstPos.dx, fourthPos.dx);
expect(firstPos.dy, lessThan(fourthPos.dy)); expect(firstPos.dy, lessThan(fourthPos.dy));
expect(inputBox.hitTest(BoxHitTestResult(), position: inputBox.globalToLocal(firstPos)), isTrue); expect(inputBox.hitTest(BoxHitTestResult(), position: inputBox.globalToLocal(firstPos)), isTrue);
...@@ -8397,10 +8392,10 @@ void main() { ...@@ -8397,10 +8392,10 @@ void main() {
), ),
), ),
); );
final Size screenSize = MediaQuery.of(tester.element(find.byType(TextField))).size;
// Just testing the test and making sure that the last character is off // Just testing the test and making sure that the last character is off
// the right side of the screen. // the right side of the screen.
expect(textOffsetToPosition(tester, 66).dx, 1056); expect(textOffsetToPosition(tester, 66).dx, greaterThan(screenSize.width));
final TestGesture gesture = final TestGesture gesture =
await tester.startGesture( await tester.startGesture(
...@@ -8448,7 +8443,7 @@ void main() { ...@@ -8448,7 +8443,7 @@ void main() {
); );
// The first character is now offscreen to the left. // The first character is now offscreen to the left.
expect(textOffsetToPosition(tester, 0).dx, moreOrLessEquals(-257.0, epsilon: 1)); expect(textOffsetToPosition(tester, 0).dx, lessThan(-100.0));
}, variant: TargetPlatformVariant.all()); }, variant: TargetPlatformVariant.all());
testWidgets('keyboard selection change scrolls the field', (WidgetTester tester) async { testWidgets('keyboard selection change scrolls the field', (WidgetTester tester) async {
...@@ -8485,7 +8480,7 @@ void main() { ...@@ -8485,7 +8480,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect( expect(
controller.selection, controller.selection,
const TextSelection.collapsed(offset: 56), const TextSelection.collapsed(offset: 56, affinity: TextAffinity.upstream),
); );
// Keep moving out. // Keep moving out.
...@@ -8495,7 +8490,7 @@ void main() { ...@@ -8495,7 +8490,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect( expect(
controller.selection, controller.selection,
const TextSelection.collapsed(offset: 62), const TextSelection.collapsed(offset: 62, affinity: TextAffinity.upstream),
); );
for (int i = 0; i < (66 - 62); i += 1) { for (int i = 0; i < (66 - 62); i += 1) {
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
...@@ -8503,7 +8498,7 @@ void main() { ...@@ -8503,7 +8498,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect( expect(
controller.selection, controller.selection,
const TextSelection.collapsed(offset: 66), const TextSelection.collapsed(offset: 66, affinity: TextAffinity.upstream),
); // We're at the edge now. ); // We're at the edge now.
await tester.pumpAndSettle(); await tester.pumpAndSettle();
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
test('Character boundary works', () {
const CharacterBoundary boundary = CharacterBoundary('abc');
const TextPosition midPosition = TextPosition(offset: 1);
expect(boundary.getLeadingTextBoundaryAt(midPosition), const TextPosition(offset: 1));
expect(boundary.getTrailingTextBoundaryAt(midPosition), const TextPosition(offset: 2, affinity: TextAffinity.upstream));
const TextPosition startPosition = TextPosition(offset: 0);
expect(boundary.getLeadingTextBoundaryAt(startPosition), const TextPosition(offset: 0));
expect(boundary.getTrailingTextBoundaryAt(startPosition), const TextPosition(offset: 1, affinity: TextAffinity.upstream));
const TextPosition endPosition = TextPosition(offset: 3);
expect(boundary.getLeadingTextBoundaryAt(endPosition), const TextPosition(offset: 3, affinity: TextAffinity.upstream));
expect(boundary.getTrailingTextBoundaryAt(endPosition), const TextPosition(offset: 3, affinity: TextAffinity.upstream));
});
test('Character boundary works with grapheme', () {
const String text = 'a❄︎c';
const CharacterBoundary boundary = CharacterBoundary(text);
TextPosition position = const TextPosition(offset: 1);
expect(boundary.getLeadingTextBoundaryAt(position), const TextPosition(offset: 1));
// The `❄` takes two character length.
expect(boundary.getTrailingTextBoundaryAt(position), const TextPosition(offset: 3, affinity: TextAffinity.upstream));
position = const TextPosition(offset: 2);
expect(boundary.getLeadingTextBoundaryAt(position), const TextPosition(offset: 1));
expect(boundary.getTrailingTextBoundaryAt(position), const TextPosition(offset: 3, affinity: TextAffinity.upstream));
position = const TextPosition(offset: 0);
expect(boundary.getLeadingTextBoundaryAt(position), const TextPosition(offset: 0));
expect(boundary.getTrailingTextBoundaryAt(position), const TextPosition(offset: 1, affinity: TextAffinity.upstream));
position = const TextPosition(offset: text.length);
expect(boundary.getLeadingTextBoundaryAt(position), const TextPosition(offset: text.length, affinity: TextAffinity.upstream));
expect(boundary.getTrailingTextBoundaryAt(position), const TextPosition(offset: text.length, affinity: TextAffinity.upstream));
});
test('word boundary works', () {
final WordBoundary boundary = WordBoundary(TestTextLayoutMetrics());
const TextPosition position = TextPosition(offset: 3);
expect(boundary.getLeadingTextBoundaryAt(position).offset, TestTextLayoutMetrics.wordBoundaryAt3.start);
expect(boundary.getTrailingTextBoundaryAt(position).offset, TestTextLayoutMetrics.wordBoundaryAt3.end);
});
test('line boundary works', () {
final LineBreak boundary = LineBreak(TestTextLayoutMetrics());
const TextPosition position = TextPosition(offset: 3);
expect(boundary.getLeadingTextBoundaryAt(position).offset, TestTextLayoutMetrics.lineAt3.start);
expect(boundary.getTrailingTextBoundaryAt(position).offset, TestTextLayoutMetrics.lineAt3.end);
});
test('document boundary works', () {
const String text = 'abcd efg hi\njklmno\npqrstuv';
const DocumentBoundary boundary = DocumentBoundary(text);
const TextPosition position = TextPosition(offset: 10);
expect(boundary.getLeadingTextBoundaryAt(position), const TextPosition(offset: 0));
expect(boundary.getTrailingTextBoundaryAt(position), const TextPosition(offset: text.length, affinity: TextAffinity.upstream));
});
}
class TestTextLayoutMetrics extends TextLayoutMetrics {
static const TextSelection lineAt3 = TextSelection(baseOffset: 0, extentOffset: 10);
static const TextRange wordBoundaryAt3 = TextRange(start: 4, end: 7);
@override
TextSelection getLineAtOffset(TextPosition position) {
if (position.offset == 3) {
return lineAt3;
}
throw UnimplementedError();
}
@override
TextPosition getTextPositionAbove(TextPosition position) {
throw UnimplementedError();
}
@override
TextPosition getTextPositionBelow(TextPosition position) {
throw UnimplementedError();
}
@override
TextRange getWordBoundary(TextPosition position) {
if (position.offset == 3) {
return wordBoundaryAt3;
}
throw UnimplementedError();
}
}
...@@ -1229,6 +1229,7 @@ void main() { ...@@ -1229,6 +1229,7 @@ void main() {
expect(controller.selection, const TextSelection.collapsed( expect(controller.selection, const TextSelection.collapsed(
offset: 21, offset: 21,
affinity: TextAffinity.upstream,
)); ));
}, variant: TargetPlatformVariant.all()); }, variant: TargetPlatformVariant.all());
...@@ -1243,6 +1244,7 @@ void main() { ...@@ -1243,6 +1244,7 @@ void main() {
expect(controller.selection, const TextSelection.collapsed( expect(controller.selection, const TextSelection.collapsed(
offset: 10, offset: 10,
affinity: TextAffinity.upstream,
)); ));
}, variant: allExceptApple); }, variant: allExceptApple);
...@@ -1353,6 +1355,7 @@ void main() { ...@@ -1353,6 +1355,7 @@ void main() {
await tester.pump(); await tester.pump();
expect(controller.selection, const TextSelection.collapsed( expect(controller.selection, const TextSelection.collapsed(
offset: 46, // After "to". offset: 46, // After "to".
affinity: TextAffinity.upstream,
)); ));
// "good" to "come" is selected. // "good" to "come" is selected.
...@@ -1365,6 +1368,7 @@ void main() { ...@@ -1365,6 +1368,7 @@ void main() {
await tester.pump(); await tester.pump();
expect(controller.selection, const TextSelection.collapsed( expect(controller.selection, const TextSelection.collapsed(
offset: 28, // After "good". offset: 28, // After "good".
affinity: TextAffinity.upstream,
)); ));
}, variant: allExceptApple); }, variant: allExceptApple);
...@@ -1673,6 +1677,7 @@ void main() { ...@@ -1673,6 +1677,7 @@ void main() {
expect(controller.selection, const TextSelection.collapsed( expect(controller.selection, const TextSelection.collapsed(
offset: 10, offset: 10,
affinity: TextAffinity.upstream,
)); ));
}, variant: macOSOnly); }, variant: macOSOnly);
...@@ -1743,6 +1748,7 @@ void main() { ...@@ -1743,6 +1748,7 @@ void main() {
await tester.pump(); await tester.pump();
expect(controller.selection, const TextSelection.collapsed( expect(controller.selection, const TextSelection.collapsed(
offset: 46, // After "to". offset: 46, // After "to".
affinity: TextAffinity.upstream,
)); ));
// "good" to "come" is selected. // "good" to "come" is selected.
...@@ -1755,6 +1761,7 @@ void main() { ...@@ -1755,6 +1761,7 @@ void main() {
await tester.pump(); await tester.pump();
expect(controller.selection, const TextSelection.collapsed( expect(controller.selection, const TextSelection.collapsed(
offset: 28, // After "good". offset: 28, // After "good".
affinity: TextAffinity.upstream,
)); ));
}, variant: macOSOnly); }, variant: macOSOnly);
......
...@@ -5832,6 +5832,7 @@ void main() { ...@@ -5832,6 +5832,7 @@ void main() {
equals( equals(
const TextSelection.collapsed( const TextSelection.collapsed(
offset: 3, offset: 3,
affinity: TextAffinity.upstream,
), ),
), ),
reason: 'on $platform', reason: 'on $platform',
...@@ -5941,6 +5942,7 @@ void main() { ...@@ -5941,6 +5942,7 @@ void main() {
const TextSelection( const TextSelection(
baseOffset: 10, baseOffset: 10,
extentOffset: 10, extentOffset: 10,
affinity: TextAffinity.upstream,
), ),
), ),
reason: 'on $platform', reason: 'on $platform',
...@@ -6398,6 +6400,7 @@ void main() { ...@@ -6398,6 +6400,7 @@ void main() {
equals( equals(
const TextSelection.collapsed( const TextSelection.collapsed(
offset: 23, offset: 23,
affinity: TextAffinity.upstream,
), ),
), ),
reason: 'on $platform', reason: 'on $platform',
...@@ -6422,6 +6425,7 @@ void main() { ...@@ -6422,6 +6425,7 @@ void main() {
equals( equals(
const TextSelection.collapsed( const TextSelection.collapsed(
offset: 23, offset: 23,
affinity: TextAffinity.upstream,
), ),
), ),
reason: 'on $platform', reason: 'on $platform',
...@@ -6464,6 +6468,7 @@ void main() { ...@@ -6464,6 +6468,7 @@ void main() {
equals( equals(
const TextSelection.collapsed( const TextSelection.collapsed(
offset: 23, offset: 23,
affinity: TextAffinity.upstream,
), ),
), ),
reason: 'on $platform', reason: 'on $platform',
...@@ -6549,6 +6554,7 @@ void main() { ...@@ -6549,6 +6554,7 @@ void main() {
equals( equals(
const TextSelection.collapsed( const TextSelection.collapsed(
offset: 32, offset: 32,
affinity: TextAffinity.upstream,
), ),
), ),
reason: 'on $platform', reason: 'on $platform',
...@@ -6573,6 +6579,7 @@ void main() { ...@@ -6573,6 +6579,7 @@ void main() {
equals( equals(
const TextSelection.collapsed( const TextSelection.collapsed(
offset: 32, offset: 32,
affinity: TextAffinity.upstream,
), ),
), ),
reason: 'on $platform', reason: 'on $platform',
...@@ -6615,6 +6622,7 @@ void main() { ...@@ -6615,6 +6622,7 @@ void main() {
equals( equals(
const TextSelection.collapsed( const TextSelection.collapsed(
offset: 32, offset: 32,
affinity: TextAffinity.upstream,
), ),
), ),
reason: 'on $platform', reason: 'on $platform',
...@@ -6710,6 +6718,7 @@ void main() { ...@@ -6710,6 +6718,7 @@ void main() {
equals( equals(
const TextSelection.collapsed( const TextSelection.collapsed(
offset: 32, offset: 32,
affinity: TextAffinity.upstream,
), ),
), ),
reason: 'on $platform', reason: 'on $platform',
...@@ -6734,6 +6743,7 @@ void main() { ...@@ -6734,6 +6743,7 @@ void main() {
equals( equals(
const TextSelection.collapsed( const TextSelection.collapsed(
offset: 32, offset: 32,
affinity: TextAffinity.upstream,
), ),
), ),
reason: 'on $platform', reason: 'on $platform',
...@@ -6776,6 +6786,7 @@ void main() { ...@@ -6776,6 +6786,7 @@ void main() {
equals( equals(
const TextSelection.collapsed( const TextSelection.collapsed(
offset: 32, offset: 32,
affinity: TextAffinity.upstream,
), ),
), ),
reason: 'on $platform', reason: 'on $platform',
...@@ -6872,6 +6883,7 @@ void main() { ...@@ -6872,6 +6883,7 @@ void main() {
equals( equals(
const TextSelection.collapsed( const TextSelection.collapsed(
offset: 23, offset: 23,
affinity: TextAffinity.upstream,
), ),
), ),
reason: 'on $platform', reason: 'on $platform',
...@@ -6917,6 +6929,7 @@ void main() { ...@@ -6917,6 +6929,7 @@ void main() {
const TextSelection( const TextSelection(
baseOffset: 23, baseOffset: 23,
extentOffset: 23, extentOffset: 23,
affinity: TextAffinity.upstream,
), ),
), ),
reason: 'on $platform', reason: 'on $platform',
...@@ -6927,6 +6940,7 @@ void main() { ...@@ -6927,6 +6940,7 @@ void main() {
const TextSelection( const TextSelection(
baseOffset: 23, baseOffset: 23,
extentOffset: 23, extentOffset: 23,
affinity: TextAffinity.upstream,
), ),
), ),
reason: 'on $platform', reason: 'on $platform',
...@@ -7060,6 +7074,7 @@ void main() { ...@@ -7060,6 +7074,7 @@ void main() {
controller.selection, controller.selection,
equals(const TextSelection.collapsed( equals(const TextSelection.collapsed(
offset: 4, offset: 4,
affinity: TextAffinity.upstream,
)), )),
); );
...@@ -7243,6 +7258,7 @@ void main() { ...@@ -7243,6 +7258,7 @@ void main() {
equals( equals(
const TextSelection.collapsed( const TextSelection.collapsed(
offset: 32, offset: 32,
affinity: TextAffinity.upstream,
), ),
), ),
reason: 'on $platform', reason: 'on $platform',
...@@ -7266,6 +7282,7 @@ void main() { ...@@ -7266,6 +7282,7 @@ void main() {
equals( equals(
const TextSelection.collapsed( const TextSelection.collapsed(
offset: 32, offset: 32,
affinity: TextAffinity.upstream,
), ),
), ),
reason: 'on $platform', reason: 'on $platform',
...@@ -7323,6 +7340,7 @@ void main() { ...@@ -7323,6 +7340,7 @@ void main() {
equals( equals(
const TextSelection.collapsed( const TextSelection.collapsed(
offset: 32, offset: 32,
affinity: TextAffinity.upstream,
), ),
), ),
reason: 'on $platform', reason: 'on $platform',
...@@ -7435,6 +7453,7 @@ void main() { ...@@ -7435,6 +7453,7 @@ void main() {
equals( equals(
const TextSelection.collapsed( const TextSelection.collapsed(
offset: 32, offset: 32,
affinity: TextAffinity.upstream,
), ),
), ),
reason: 'on $platform', reason: 'on $platform',
...@@ -7458,6 +7477,7 @@ void main() { ...@@ -7458,6 +7477,7 @@ void main() {
equals( equals(
const TextSelection.collapsed( const TextSelection.collapsed(
offset: 32, offset: 32,
affinity: TextAffinity.upstream,
), ),
), ),
reason: 'on $platform', reason: 'on $platform',
...@@ -7515,6 +7535,7 @@ void main() { ...@@ -7515,6 +7535,7 @@ void main() {
equals( equals(
const TextSelection.collapsed( const TextSelection.collapsed(
offset: 32, offset: 32,
affinity: TextAffinity.upstream,
), ),
), ),
reason: 'on $platform', reason: 'on $platform',
...@@ -7626,6 +7647,7 @@ void main() { ...@@ -7626,6 +7647,7 @@ void main() {
equals( equals(
const TextSelection.collapsed( const TextSelection.collapsed(
offset: 32, offset: 32,
affinity: TextAffinity.upstream,
), ),
), ),
); );
...@@ -10383,7 +10405,6 @@ void main() { ...@@ -10383,7 +10405,6 @@ void main() {
expect(controller.selection.isCollapsed, false); expect(controller.selection.isCollapsed, false);
expect(controller.selection.baseOffset, 7); expect(controller.selection.baseOffset, 7);
expect(controller.selection.extentOffset, 10); expect(controller.selection.extentOffset, 10);
await sendKeys( await sendKeys(
tester, tester,
<LogicalKeyboardKey>[LogicalKeyboardKey.arrowLeft], <LogicalKeyboardKey>[LogicalKeyboardKey.arrowLeft],
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -42,7 +43,7 @@ Offset textOffsetToPosition(WidgetTester tester, int offset) { ...@@ -42,7 +43,7 @@ Offset textOffsetToPosition(WidgetTester tester, int offset) {
renderEditable, renderEditable,
); );
expect(endpoints.length, 1); expect(endpoints.length, 1);
return endpoints[0].point + const Offset(0.0, -2.0); return endpoints[0].point + const Offset(kIsWeb? 1.0 : 0.0, -2.0);
} }
// Simple controller that builds a WidgetSpan with 100 height. // Simple controller that builds a WidgetSpan with 100 height.
......
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