Unverified Commit 0a2e0a47 authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

Avoid using `TextAffinity` in `TextBoundary` (#117446)

* Avoid affinity like the plague

* ignore lint

* clean up

* fix test

* review

* Move wordboundary to text painter

* docs

* fix tests
parent 5a87a829
...@@ -144,6 +144,133 @@ enum TextWidthBasis { ...@@ -144,6 +144,133 @@ enum TextWidthBasis {
longestLine, longestLine,
} }
/// A [TextBoundary] subclass for locating word breaks.
///
/// The underlying implementation uses [UAX #29](https://unicode.org/reports/tr29/)
/// defined default word boundaries.
///
/// The default word break rules can be tailored to meet the requirements of
/// different use cases. For instance, the default rule set keeps horizontal
/// whitespaces together as a single word, which may not make sense in a
/// word-counting context -- "hello world" counts as 3 words instead of 2.
/// An example is the [moveByWordBoundary] variant, which is a tailored
/// word-break locator that more closely matches the default behavior of most
/// platforms and editors when it comes to handling text editing keyboard
/// shortcuts that move or delete word by word.
class WordBoundary extends TextBoundary {
/// Creates a [WordBoundary] with the text and layout information.
WordBoundary._(this._text, this._paragraph);
final InlineSpan _text;
final ui.Paragraph _paragraph;
@override
TextRange getTextBoundaryAt(int position) => _paragraph.getWordBoundary(TextPosition(offset: max(position, 0)));
// Combines two UTF-16 code units (high surrogate + low surrogate) into a
// single code point that represents a supplementary character.
static int _codePointFromSurrogates(int highSurrogate, int lowSurrogate) {
assert(
TextPainter._isHighSurrogate(highSurrogate),
'U+${highSurrogate.toRadixString(16).toUpperCase().padLeft(4, "0")}) is not a high surrogate.',
);
assert(
TextPainter._isLowSurrogate(lowSurrogate),
'U+${lowSurrogate.toRadixString(16).toUpperCase().padLeft(4, "0")}) is not a low surrogate.',
);
const int base = 0x010000 - (0xD800 << 10) - 0xDC00;
return (highSurrogate << 10) + lowSurrogate + base;
}
// The Runes class does not provide random access with a code unit offset.
int? _codePointAt(int index) {
final int? codeUnitAtIndex = _text.codeUnitAt(index);
if (codeUnitAtIndex == null) {
return null;
}
switch (codeUnitAtIndex & 0xFC00) {
case 0xD800:
return _codePointFromSurrogates(codeUnitAtIndex, _text.codeUnitAt(index + 1)!);
case 0xDC00:
return _codePointFromSurrogates(_text.codeUnitAt(index - 1)!, codeUnitAtIndex);
default:
return codeUnitAtIndex;
}
}
static bool _isNewline(int codePoint) {
switch (codePoint) {
case 0x000A:
case 0x0085:
case 0x000B:
case 0x000C:
case 0x2028:
case 0x2029:
return true;
default:
return false;
}
}
bool _skipSpacesAndPunctuations(int offset, bool forward) {
// Use code point since some punctuations are supplementary characters.
// "inner" here refers to the code unit that's before the break in the
// search direction (`forward`).
final int? innerCodePoint = _codePointAt(forward ? offset - 1 : offset);
final int? outerCodeUnit = _text.codeUnitAt(forward ? offset : offset - 1);
// Make sure the hard break rules in UAX#29 take precedence over the ones we
// add below. Luckily there're only 4 hard break rules for word breaks, and
// dictionary based breaking does not introduce new hard breaks:
// https://unicode-org.github.io/icu/userguide/boundaryanalysis/break-rules.html#word-dictionaries
//
// WB1 & WB2: always break at the start or the end of the text.
final bool hardBreakRulesApply = innerCodePoint == null || outerCodeUnit == null
// WB3a & WB3b: always break before and after newlines.
|| _isNewline(innerCodePoint) || _isNewline(outerCodeUnit);
return hardBreakRulesApply || !RegExp(r'[\p{Space_Separator}\p{Punctuation}]', unicode: true).hasMatch(String.fromCharCode(innerCodePoint));
}
/// Returns a [TextBoundary] suitable for handling keyboard navigation
/// commands that change the current selection word by word.
///
/// This [TextBoundary] is used by text widgets in the flutter framework to
/// provide default implementation for text editing shortcuts, for example,
/// "delete to the previous word".
///
/// The implementation applies the same set of rules [WordBoundary] uses,
/// except that word breaks end on a space separator or a punctuation will be
/// skipped, to match the behavior of most platforms. Additional rules may be
/// added in the future to better match platform behaviors.
late final TextBoundary moveByWordBoundary = _UntilTextBoundary(this, _skipSpacesAndPunctuations);
}
class _UntilTextBoundary extends TextBoundary {
const _UntilTextBoundary(this._textBoundary, this._predicate);
final UntilPredicate _predicate;
final TextBoundary _textBoundary;
@override
int? getLeadingTextBoundaryAt(int position) {
if (position < 0) {
return null;
}
final int? offset = _textBoundary.getLeadingTextBoundaryAt(position);
return offset == null || _predicate(offset, false)
? offset
: getLeadingTextBoundaryAt(offset - 1);
}
@override
int? getTrailingTextBoundaryAt(int position) {
final int? offset = _textBoundary.getTrailingTextBoundaryAt(max(position, 0));
return offset == null || _predicate(offset, true)
? offset
: getTrailingTextBoundaryAt(offset);
}
}
/// This is used to cache and pass the computed metrics regarding the /// This is used to cache and pass the computed metrics regarding the
/// caret's size and position. This is preferred due to the expensive /// caret's size and position. This is preferred due to the expensive
/// nature of the calculation. /// nature of the calculation.
...@@ -750,7 +877,7 @@ class TextPainter { ...@@ -750,7 +877,7 @@ class TextPainter {
// Creates a ui.Paragraph using the current configurations in this class and // Creates a ui.Paragraph using the current configurations in this class and
// assign it to _paragraph. // assign it to _paragraph.
void _createParagraph() { ui.Paragraph _createParagraph() {
assert(_paragraph == null || _rebuildParagraphForPaint); assert(_paragraph == null || _rebuildParagraphForPaint);
final InlineSpan? text = this.text; final InlineSpan? text = this.text;
if (text == null) { if (text == null) {
...@@ -763,8 +890,9 @@ class TextPainter { ...@@ -763,8 +890,9 @@ class TextPainter {
_debugMarkNeedsLayoutCallStack = null; _debugMarkNeedsLayoutCallStack = null;
return true; return true;
}()); }());
_paragraph = builder.build(); final ui.Paragraph paragraph = _paragraph = builder.build();
_rebuildParagraphForPaint = false; _rebuildParagraphForPaint = false;
return paragraph;
} }
void _layoutParagraph(double minWidth, double maxWidth) { void _layoutParagraph(double minWidth, double maxWidth) {
...@@ -861,13 +989,18 @@ class TextPainter { ...@@ -861,13 +989,18 @@ class TextPainter {
canvas.drawParagraph(_paragraph!, offset); canvas.drawParagraph(_paragraph!, offset);
} }
// Returns true iff the given value is a valid UTF-16 surrogate. The value // Returns true iff the given value is a valid UTF-16 high surrogate. The value
// must be a UTF-16 code unit, meaning it must be in the range 0x0000-0xFFFF. // must be a UTF-16 code unit, meaning it must be in the range 0x0000-0xFFFF.
// //
// See also: // See also:
// * https://en.wikipedia.org/wiki/UTF-16#Code_points_from_U+010000_to_U+10FFFF // * https://en.wikipedia.org/wiki/UTF-16#Code_points_from_U+010000_to_U+10FFFF
static bool _isUtf16Surrogate(int value) { static bool _isHighSurrogate(int value) {
return value & 0xF800 == 0xD800; return value & 0xFC00 == 0xD800;
}
// Whether the given UTF-16 code unit is a low (second) surrogate.
static bool _isLowSurrogate(int value) {
return value & 0xFC00 == 0xDC00;
} }
// Checks if the glyph is either [Unicode.RLM] or [Unicode.LRM]. These values take // Checks if the glyph is either [Unicode.RLM] or [Unicode.LRM]. These values take
...@@ -886,7 +1019,7 @@ class TextPainter { ...@@ -886,7 +1019,7 @@ class TextPainter {
return null; return null;
} }
// TODO(goderbauer): doesn't handle extended grapheme clusters with more than one Unicode scalar value (https://github.com/flutter/flutter/issues/13404). // TODO(goderbauer): doesn't handle extended grapheme clusters with more than one Unicode scalar value (https://github.com/flutter/flutter/issues/13404).
return _isUtf16Surrogate(nextCodeUnit) ? offset + 2 : offset + 1; return _isHighSurrogate(nextCodeUnit) ? offset + 2 : offset + 1;
} }
/// Returns the closest offset before `offset` at which the input cursor can /// Returns the closest offset before `offset` at which the input cursor can
...@@ -897,7 +1030,7 @@ class TextPainter { ...@@ -897,7 +1030,7 @@ class TextPainter {
return null; return null;
} }
// TODO(goderbauer): doesn't handle extended grapheme clusters with more than one Unicode scalar value (https://github.com/flutter/flutter/issues/13404). // TODO(goderbauer): doesn't handle extended grapheme clusters with more than one Unicode scalar value (https://github.com/flutter/flutter/issues/13404).
return _isUtf16Surrogate(prevCodeUnit) ? offset - 2 : offset - 1; return _isLowSurrogate(prevCodeUnit) ? offset - 2 : offset - 1;
} }
// Unicode value for a zero width joiner character. // Unicode value for a zero width joiner character.
...@@ -916,7 +1049,7 @@ class TextPainter { ...@@ -916,7 +1049,7 @@ class TextPainter {
const int NEWLINE_CODE_UNIT = 10; const int NEWLINE_CODE_UNIT = 10;
// Check for multi-code-unit glyphs such as emojis or zero width joiner. // Check for multi-code-unit glyphs such as emojis or zero width joiner.
final bool needsSearch = _isUtf16Surrogate(prevCodeUnit) || _text!.codeUnitAt(offset) == _zwjUtf16 || _isUnicodeDirectionality(prevCodeUnit); final bool needsSearch = _isHighSurrogate(prevCodeUnit) || _isLowSurrogate(prevCodeUnit) || _text!.codeUnitAt(offset) == _zwjUtf16 || _isUnicodeDirectionality(prevCodeUnit);
int graphemeClusterLength = needsSearch ? 2 : 1; int graphemeClusterLength = needsSearch ? 2 : 1;
List<TextBox> boxes = <TextBox>[]; List<TextBox> boxes = <TextBox>[];
while (boxes.isEmpty) { while (boxes.isEmpty) {
...@@ -966,7 +1099,7 @@ class TextPainter { ...@@ -966,7 +1099,7 @@ class TextPainter {
final int nextCodeUnit = plainText.codeUnitAt(min(offset, plainTextLength - 1)); final int nextCodeUnit = plainText.codeUnitAt(min(offset, plainTextLength - 1));
// Check for multi-code-unit glyphs such as emojis or zero width joiner // Check for multi-code-unit glyphs such as emojis or zero width joiner
final bool needsSearch = _isUtf16Surrogate(nextCodeUnit) || nextCodeUnit == _zwjUtf16 || _isUnicodeDirectionality(nextCodeUnit); final bool needsSearch = _isHighSurrogate(nextCodeUnit) || _isLowSurrogate(nextCodeUnit) || nextCodeUnit == _zwjUtf16 || _isUnicodeDirectionality(nextCodeUnit);
int graphemeClusterLength = needsSearch ? 2 : 1; int graphemeClusterLength = needsSearch ? 2 : 1;
List<TextBox> boxes = <TextBox>[]; List<TextBox> boxes = <TextBox>[];
while (boxes.isEmpty) { while (boxes.isEmpty) {
...@@ -1141,6 +1274,18 @@ class TextPainter { ...@@ -1141,6 +1274,18 @@ class TextPainter {
return _paragraph!.getWordBoundary(position); return _paragraph!.getWordBoundary(position);
} }
/// {@template flutter.painting.TextPainter.wordBoundaries}
/// Returns a [TextBoundary] that can be used to perform word boundary analysis
/// on the current [text].
///
/// This [TextBoundary] uses word boundary rules defined in [Unicode Standard
/// Annex #29](http://www.unicode.org/reports/tr29/#Word_Boundaries).
/// {@endtemplate}
///
/// Currently word boundary analysis can only be performed after [layout]
/// has been called.
WordBoundary get wordBoundaries => WordBoundary._(text!, _paragraph!);
/// Returns the text range of the line at the given offset. /// Returns the text range of the line at the given offset.
/// ///
/// The newline (if any) is not returned as part of the range. /// The newline (if any) is not returned as part of the range.
......
...@@ -2098,6 +2098,9 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, ...@@ -2098,6 +2098,9 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
_setSelection(newSelection, cause); _setSelection(newSelection, cause);
} }
/// {@macro flutter.painting.TextPainter.wordBoundaries}
WordBoundary get wordBoundaries => _textPainter.wordBoundaries;
/// Select a word around the location of the last tap down. /// Select a word around the location of the last tap down.
/// ///
/// {@macro flutter.rendering.RenderEditable.selectPosition} /// {@macro flutter.rendering.RenderEditable.selectPosition}
......
...@@ -1559,21 +1559,21 @@ class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutM ...@@ -1559,21 +1559,21 @@ class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutM
switch (granularity) { switch (granularity) {
case TextGranularity.character: case TextGranularity.character:
final String text = range.textInside(fullText); final String text = range.textInside(fullText);
newPosition = _getNextPosition(CharacterBoundary(text), targetedEdge, forward); newPosition = _moveBeyondTextBoundaryAtDirection(targetedEdge, forward, CharacterBoundary(text));
result = SelectionResult.end; result = SelectionResult.end;
break; break;
case TextGranularity.word: case TextGranularity.word:
final String text = range.textInside(fullText); final TextBoundary textBoundary = paragraph._textPainter.wordBoundaries.moveByWordBoundary;
newPosition = _getNextPosition(WhitespaceBoundary(text) + WordBoundary(this), targetedEdge, forward); newPosition = _moveBeyondTextBoundaryAtDirection(targetedEdge, forward, textBoundary);
result = SelectionResult.end; result = SelectionResult.end;
break; break;
case TextGranularity.line: case TextGranularity.line:
newPosition = _getNextPosition(LineBreak(this), targetedEdge, forward); newPosition = _moveToTextBoundaryAtDirection(targetedEdge, forward, LineBoundary(this));
result = SelectionResult.end; result = SelectionResult.end;
break; break;
case TextGranularity.document: case TextGranularity.document:
final String text = range.textInside(fullText); final String text = range.textInside(fullText);
newPosition = _getNextPosition(DocumentBoundary(text), targetedEdge, forward); newPosition = _moveBeyondTextBoundaryAtDirection(targetedEdge, forward, DocumentBoundary(text));
if (forward && newPosition.offset == range.end) { if (forward && newPosition.offset == range.end) {
result = SelectionResult.next; result = SelectionResult.next;
} else if (!forward && newPosition.offset == range.start) { } else if (!forward && newPosition.offset == range.start) {
...@@ -1592,15 +1592,43 @@ class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutM ...@@ -1592,15 +1592,43 @@ class _SelectableFragment with Selectable, ChangeNotifier implements TextLayoutM
return result; return result;
} }
TextPosition _getNextPosition(TextBoundary boundary, TextPosition position, bool forward) { // Move **beyond** the local boundary of the given type (unless range.start or
if (forward) { // range.end is reached). Used for most TextGranularity types except for
return _clampTextPosition( // TextGranularity.line, to ensure the selection movement doesn't get stuck at
(PushTextPosition.forward + boundary).getTrailingTextBoundaryAt(position) // a local fixed point.
); TextPosition _moveBeyondTextBoundaryAtDirection(TextPosition end, bool forward, TextBoundary textBoundary) {
final int newOffset = forward
? textBoundary.getTrailingTextBoundaryAt(end.offset) ?? range.end
: textBoundary.getLeadingTextBoundaryAt(end.offset - 1) ?? range.start;
return TextPosition(offset: newOffset);
}
// Move **to** the local boundary of the given type. Typically used for line
// boundaries, such that performing "move to line start" more than once never
// moves the selection to the previous line.
TextPosition _moveToTextBoundaryAtDirection(TextPosition end, bool forward, TextBoundary textBoundary) {
assert(end.offset >= 0);
final int caretOffset;
switch (end.affinity) {
case TextAffinity.upstream:
if (end.offset < 1 && !forward) {
assert (end.offset == 0);
return const TextPosition(offset: 0);
}
final CharacterBoundary characterBoundary = CharacterBoundary(fullText);
caretOffset = math.max(
0,
characterBoundary.getLeadingTextBoundaryAt(range.start + end.offset) ?? range.start,
) - 1;
break;
case TextAffinity.downstream:
caretOffset = end.offset;
break;
} }
return _clampTextPosition( final int offset = forward
(PushTextPosition.backward + boundary).getLeadingTextBoundaryAt(position), ? textBoundary.getTrailingTextBoundaryAt(caretOffset) ?? range.end
); : textBoundary.getLeadingTextBoundaryAt(caretOffset) ?? range.start;
return TextPosition(offset: offset);
} }
MapEntry<TextPosition, SelectionResult> _handleVerticalMovement(TextPosition position, {required double horizontalBaselineInParagraphCoordinates, required bool below}) { MapEntry<TextPosition, SelectionResult> _handleVerticalMovement(TextPosition position, {required double horizontalBaselineInParagraphCoordinates, required bool below}) {
......
...@@ -137,11 +137,11 @@ class ExtendSelectionToNextWordBoundaryIntent extends DirectionalCaretMovementIn ...@@ -137,11 +137,11 @@ class ExtendSelectionToNextWordBoundaryIntent extends DirectionalCaretMovementIn
/// reverse. /// reverse.
/// ///
/// This is typically only used on MacOS. /// This is typically only used on MacOS.
class ExtendSelectionToNextWordBoundaryOrCaretLocationIntent extends DirectionalTextEditingIntent { class ExtendSelectionToNextWordBoundaryOrCaretLocationIntent extends DirectionalCaretMovementIntent {
/// Creates an [ExtendSelectionToNextWordBoundaryOrCaretLocationIntent]. /// Creates an [ExtendSelectionToNextWordBoundaryOrCaretLocationIntent].
const ExtendSelectionToNextWordBoundaryOrCaretLocationIntent({ const ExtendSelectionToNextWordBoundaryOrCaretLocationIntent({
required bool forward, required bool forward,
}) : super(forward); }) : super(forward, false, true);
} }
/// Expands the current selection to the document boundary in the direction /// Expands the current selection to the document boundary in the direction
...@@ -154,11 +154,11 @@ class ExtendSelectionToNextWordBoundaryOrCaretLocationIntent extends Directional ...@@ -154,11 +154,11 @@ class ExtendSelectionToNextWordBoundaryOrCaretLocationIntent extends Directional
/// ///
/// [ExtendSelectionToDocumentBoundaryIntent], which is similar but always /// [ExtendSelectionToDocumentBoundaryIntent], which is similar but always
/// moves the extent. /// moves the extent.
class ExpandSelectionToDocumentBoundaryIntent extends DirectionalTextEditingIntent { class ExpandSelectionToDocumentBoundaryIntent extends DirectionalCaretMovementIntent {
/// Creates an [ExpandSelectionToDocumentBoundaryIntent]. /// Creates an [ExpandSelectionToDocumentBoundaryIntent].
const ExpandSelectionToDocumentBoundaryIntent({ const ExpandSelectionToDocumentBoundaryIntent({
required bool forward, required bool forward,
}) : super(forward); }) : super(forward, false);
} }
/// Expands the current selection to the closest line break in the direction /// Expands the current selection to the closest line break in the direction
...@@ -173,11 +173,11 @@ class ExpandSelectionToDocumentBoundaryIntent extends DirectionalTextEditingInte ...@@ -173,11 +173,11 @@ class ExpandSelectionToDocumentBoundaryIntent extends DirectionalTextEditingInte
/// ///
/// [ExtendSelectionToLineBreakIntent], which is similar but always moves the /// [ExtendSelectionToLineBreakIntent], which is similar but always moves the
/// extent. /// extent.
class ExpandSelectionToLineBreakIntent extends DirectionalTextEditingIntent { class ExpandSelectionToLineBreakIntent extends DirectionalCaretMovementIntent {
/// Creates an [ExpandSelectionToLineBreakIntent]. /// Creates an [ExpandSelectionToLineBreakIntent].
const ExpandSelectionToLineBreakIntent({ const ExpandSelectionToLineBreakIntent({
required bool forward, required bool forward,
}) : super(forward); }) : super(forward, false);
} }
/// Extends, or moves the current selection from the current /// Extends, or moves the current selection from the current
......
...@@ -9785,7 +9785,8 @@ void main() { ...@@ -9785,7 +9785,8 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect( expect(
controller.selection, controller.selection,
const TextSelection.collapsed(offset: 56, affinity: TextAffinity.upstream), // arrowRight always sets the affinity to downstream.
const TextSelection.collapsed(offset: 56),
); );
// Keep moving out. // Keep moving out.
...@@ -9795,7 +9796,7 @@ void main() { ...@@ -9795,7 +9796,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect( expect(
controller.selection, controller.selection,
const TextSelection.collapsed(offset: 62, affinity: TextAffinity.upstream), const TextSelection.collapsed(offset: 62),
); );
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);
...@@ -9803,7 +9804,7 @@ void main() { ...@@ -9803,7 +9804,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect( expect(
controller.selection, controller.selection,
const TextSelection.collapsed(offset: 66, affinity: TextAffinity.upstream), const TextSelection.collapsed(offset: 66),
); // We're at the edge now. ); // We're at the edge now.
await tester.pumpAndSettle(); await tester.pumpAndSettle();
......
...@@ -1075,8 +1075,13 @@ void main() { ...@@ -1075,8 +1075,13 @@ void main() {
), ),
); );
selection = paragraph.selections[0]; selection = paragraph.selections[0];
expect(selection.start, 0); // [how ]are you if (isBrowser && !isCanvasKit) {
expect(selection.end, 4); // how [are you\n]
expect(selection, const TextRange(start: 4, end: 12));
} else {
// [how ]are you
expect(selection, const TextRange(start: 0, end: 4));
}
}); });
test('can granularly extend selection - document', () async { test('can granularly extend selection - document', () async {
......
...@@ -45,7 +45,7 @@ void main() { ...@@ -45,7 +45,7 @@ void main() {
); );
} }
group('iOS: do not delete/backspace events', () { group('iOS: do not handle delete/backspace events', () {
final TargetPlatformVariant iOS = TargetPlatformVariant.only(TargetPlatform.iOS); final TargetPlatformVariant iOS = TargetPlatformVariant.only(TargetPlatform.iOS);
final FocusNode editable = FocusNode(); final FocusNode editable = FocusNode();
final FocusNode spy = FocusNode(); final FocusNode spy = FocusNode();
......
...@@ -576,12 +576,12 @@ void main() { ...@@ -576,12 +576,12 @@ void main() {
'Now is the time for\n' 'Now is the time for\n'
'all good people\n' 'all good people\n'
'to come to the aid\n' 'to come to the aid\n'
'of their country', 'of their ',
); );
expect( expect(
controller.selection, controller.selection,
const TextSelection.collapsed(offset: 71), const TextSelection.collapsed(offset: 64),
); );
}, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS })); }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));
...@@ -795,7 +795,7 @@ void main() { ...@@ -795,7 +795,7 @@ void main() {
testWidgets('softwrap line boundary, upstream', (WidgetTester tester) async { testWidgets('softwrap line boundary, upstream', (WidgetTester tester) async {
controller.text = testSoftwrapText; controller.text = testSoftwrapText;
// Place the caret at the beginning of the 3rd line. // Place the caret at the end of the 2nd line.
controller.selection = const TextSelection.collapsed( controller.selection = const TextSelection.collapsed(
offset: 40, offset: 40,
affinity: TextAffinity.upstream, affinity: TextAffinity.upstream,
...@@ -827,12 +827,11 @@ void main() { ...@@ -827,12 +827,11 @@ void main() {
await tester.pumpWidget(buildEditableText()); await tester.pumpWidget(buildEditableText());
await sendKeyCombination(tester, lineModifierBackspace()); await sendKeyCombination(tester, lineModifierBackspace());
expect(controller.text, testSoftwrapText);
expect( expect(
controller.selection, controller.selection,
const TextSelection.collapsed(offset: 40), const TextSelection.collapsed(offset: 40),
); );
expect(controller.text, testSoftwrapText);
}, variant: TargetPlatformVariant.all()); }, variant: TargetPlatformVariant.all());
testWidgets('readonly', (WidgetTester tester) async { testWidgets('readonly', (WidgetTester tester) async {
...@@ -976,7 +975,7 @@ void main() { ...@@ -976,7 +975,7 @@ void main() {
testWidgets('softwrap line boundary, upstream', (WidgetTester tester) async { testWidgets('softwrap line boundary, upstream', (WidgetTester tester) async {
controller.text = testSoftwrapText; controller.text = testSoftwrapText;
// Place the caret at the beginning of the 3rd line. // Place the caret at the end of the 2nd line.
controller.selection = const TextSelection.collapsed( controller.selection = const TextSelection.collapsed(
offset: 40, offset: 40,
affinity: TextAffinity.upstream, affinity: TextAffinity.upstream,
...@@ -1215,7 +1214,6 @@ void main() { ...@@ -1215,7 +1214,6 @@ 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());
...@@ -1230,7 +1228,6 @@ void main() { ...@@ -1230,7 +1228,6 @@ 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);
...@@ -1341,7 +1338,6 @@ void main() { ...@@ -1341,7 +1338,6 @@ 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.
...@@ -1354,7 +1350,6 @@ void main() { ...@@ -1354,7 +1350,6 @@ 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);
...@@ -1718,8 +1713,7 @@ void main() { ...@@ -1718,8 +1713,7 @@ void main() {
await tester.pump(); await tester.pump();
expect(controller.selection, const TextSelection.collapsed( expect(controller.selection, const TextSelection.collapsed(
offset: 10, offset: 10, // after the first "the"
affinity: TextAffinity.upstream,
)); ));
}, variant: macOSOnly); }, variant: macOSOnly);
...@@ -1790,7 +1784,6 @@ void main() { ...@@ -1790,7 +1784,6 @@ 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.
...@@ -1803,7 +1796,6 @@ void main() { ...@@ -1803,7 +1796,6 @@ 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);
...@@ -2351,9 +2343,7 @@ void main() { ...@@ -2351,9 +2343,7 @@ void main() {
expect(controller.text, 'testing'); expect(controller.text, 'testing');
expect( expect(
controller.selection, controller.selection,
const TextSelection.collapsed( const TextSelection.collapsed(offset: 6), // should not expand selection
offset: 6,
affinity: TextAffinity.upstream), // should not expand selection
reason: selectRight.toString(), reason: selectRight.toString(),
); );
}, variant: TargetPlatformVariant.desktop()); }, variant: TargetPlatformVariant.desktop());
......
...@@ -5695,7 +5695,6 @@ void main() { ...@@ -5695,7 +5695,6 @@ void main() {
const TextSelection( const TextSelection(
baseOffset: 0, baseOffset: 0,
extentOffset: 6, extentOffset: 6,
affinity: TextAffinity.upstream,
), ),
), ),
reason: 'on $platform', reason: 'on $platform',
...@@ -5957,7 +5956,6 @@ void main() { ...@@ -5957,7 +5956,6 @@ void main() {
equals( equals(
const TextSelection.collapsed( const TextSelection.collapsed(
offset: testText.length, offset: testText.length,
affinity: TextAffinity.upstream,
), ),
), ),
reason: 'on $platform', reason: 'on $platform',
...@@ -6001,7 +5999,6 @@ void main() { ...@@ -6001,7 +5999,6 @@ void main() {
equals( equals(
const TextSelection.collapsed( const TextSelection.collapsed(
offset: 3, offset: 3,
affinity: TextAffinity.upstream,
), ),
), ),
reason: 'on $platform', reason: 'on $platform',
...@@ -6193,7 +6190,6 @@ void main() { ...@@ -6193,7 +6190,6 @@ void main() {
const TextSelection( const TextSelection(
baseOffset: 10, baseOffset: 10,
extentOffset: 10, extentOffset: 10,
affinity: TextAffinity.upstream,
), ),
), ),
reason: 'on $platform', reason: 'on $platform',
...@@ -6653,7 +6649,6 @@ void main() { ...@@ -6653,7 +6649,6 @@ void main() {
equals( equals(
const TextSelection.collapsed( const TextSelection.collapsed(
offset: 23, offset: 23,
affinity: TextAffinity.upstream,
), ),
), ),
reason: 'on $platform', reason: 'on $platform',
...@@ -6678,7 +6673,6 @@ void main() { ...@@ -6678,7 +6673,6 @@ void main() {
equals( equals(
const TextSelection.collapsed( const TextSelection.collapsed(
offset: 23, offset: 23,
affinity: TextAffinity.upstream,
), ),
), ),
reason: 'on $platform', reason: 'on $platform',
...@@ -6721,7 +6715,6 @@ void main() { ...@@ -6721,7 +6715,6 @@ void main() {
equals( equals(
const TextSelection.collapsed( const TextSelection.collapsed(
offset: 23, offset: 23,
affinity: TextAffinity.upstream,
), ),
), ),
reason: 'on $platform', reason: 'on $platform',
...@@ -6807,7 +6800,6 @@ void main() { ...@@ -6807,7 +6800,6 @@ void main() {
equals( equals(
const TextSelection.collapsed( const TextSelection.collapsed(
offset: 32, offset: 32,
affinity: TextAffinity.upstream,
), ),
), ),
reason: 'on $platform', reason: 'on $platform',
...@@ -6832,7 +6824,6 @@ void main() { ...@@ -6832,7 +6824,6 @@ void main() {
equals( equals(
const TextSelection.collapsed( const TextSelection.collapsed(
offset: 32, offset: 32,
affinity: TextAffinity.upstream,
), ),
), ),
reason: 'on $platform', reason: 'on $platform',
...@@ -6875,7 +6866,6 @@ void main() { ...@@ -6875,7 +6866,6 @@ void main() {
equals( equals(
const TextSelection.collapsed( const TextSelection.collapsed(
offset: 32, offset: 32,
affinity: TextAffinity.upstream,
), ),
), ),
reason: 'on $platform', reason: 'on $platform',
...@@ -6971,7 +6961,6 @@ void main() { ...@@ -6971,7 +6961,6 @@ void main() {
equals( equals(
const TextSelection.collapsed( const TextSelection.collapsed(
offset: 32, offset: 32,
affinity: TextAffinity.upstream,
), ),
), ),
reason: 'on $platform', reason: 'on $platform',
...@@ -6996,7 +6985,6 @@ void main() { ...@@ -6996,7 +6985,6 @@ void main() {
equals( equals(
const TextSelection.collapsed( const TextSelection.collapsed(
offset: 32, offset: 32,
affinity: TextAffinity.upstream,
), ),
), ),
reason: 'on $platform', reason: 'on $platform',
...@@ -7039,7 +7027,6 @@ void main() { ...@@ -7039,7 +7027,6 @@ void main() {
equals( equals(
const TextSelection.collapsed( const TextSelection.collapsed(
offset: 32, offset: 32,
affinity: TextAffinity.upstream,
), ),
), ),
reason: 'on $platform', reason: 'on $platform',
...@@ -7136,7 +7123,6 @@ void main() { ...@@ -7136,7 +7123,6 @@ void main() {
equals( equals(
const TextSelection.collapsed( const TextSelection.collapsed(
offset: 23, offset: 23,
affinity: TextAffinity.upstream,
), ),
), ),
reason: 'on $platform', reason: 'on $platform',
...@@ -7182,7 +7168,6 @@ void main() { ...@@ -7182,7 +7168,6 @@ void main() {
const TextSelection( const TextSelection(
baseOffset: 23, baseOffset: 23,
extentOffset: 23, extentOffset: 23,
affinity: TextAffinity.upstream,
), ),
), ),
reason: 'on $platform', reason: 'on $platform',
...@@ -7193,7 +7178,6 @@ void main() { ...@@ -7193,7 +7178,6 @@ void main() {
const TextSelection( const TextSelection(
baseOffset: 23, baseOffset: 23,
extentOffset: 23, extentOffset: 23,
affinity: TextAffinity.upstream,
), ),
), ),
reason: 'on $platform', reason: 'on $platform',
...@@ -7327,7 +7311,6 @@ void main() { ...@@ -7327,7 +7311,6 @@ void main() {
controller.selection, controller.selection,
equals(const TextSelection.collapsed( equals(const TextSelection.collapsed(
offset: 4, offset: 4,
affinity: TextAffinity.upstream,
)), )),
); );
...@@ -7511,7 +7494,6 @@ void main() { ...@@ -7511,7 +7494,6 @@ void main() {
equals( equals(
const TextSelection.collapsed( const TextSelection.collapsed(
offset: 32, offset: 32,
affinity: TextAffinity.upstream,
), ),
), ),
reason: 'on $platform', reason: 'on $platform',
...@@ -7535,7 +7517,6 @@ void main() { ...@@ -7535,7 +7517,6 @@ void main() {
equals( equals(
const TextSelection.collapsed( const TextSelection.collapsed(
offset: 32, offset: 32,
affinity: TextAffinity.upstream,
), ),
), ),
reason: 'on $platform', reason: 'on $platform',
...@@ -7593,7 +7574,6 @@ void main() { ...@@ -7593,7 +7574,6 @@ void main() {
equals( equals(
const TextSelection.collapsed( const TextSelection.collapsed(
offset: 32, offset: 32,
affinity: TextAffinity.upstream,
), ),
), ),
reason: 'on $platform', reason: 'on $platform',
...@@ -7706,7 +7686,6 @@ void main() { ...@@ -7706,7 +7686,6 @@ void main() {
equals( equals(
const TextSelection.collapsed( const TextSelection.collapsed(
offset: 32, offset: 32,
affinity: TextAffinity.upstream,
), ),
), ),
reason: 'on $platform', reason: 'on $platform',
...@@ -7730,7 +7709,6 @@ void main() { ...@@ -7730,7 +7709,6 @@ void main() {
equals( equals(
const TextSelection.collapsed( const TextSelection.collapsed(
offset: 32, offset: 32,
affinity: TextAffinity.upstream,
), ),
), ),
reason: 'on $platform', reason: 'on $platform',
...@@ -7788,7 +7766,6 @@ void main() { ...@@ -7788,7 +7766,6 @@ void main() {
equals( equals(
const TextSelection.collapsed( const TextSelection.collapsed(
offset: 32, offset: 32,
affinity: TextAffinity.upstream,
), ),
), ),
reason: 'on $platform', reason: 'on $platform',
...@@ -7900,7 +7877,6 @@ void main() { ...@@ -7900,7 +7877,6 @@ void main() {
equals( equals(
const TextSelection.collapsed( const TextSelection.collapsed(
offset: 32, offset: 32,
affinity: TextAffinity.upstream,
), ),
), ),
); );
...@@ -7994,7 +7970,6 @@ void main() { ...@@ -7994,7 +7970,6 @@ void main() {
controller.selection, controller.selection,
equals(const TextSelection.collapsed( equals(const TextSelection.collapsed(
offset: testText.length, offset: testText.length,
affinity: TextAffinity.upstream,
)), )),
); );
...@@ -8078,7 +8053,6 @@ void main() { ...@@ -8078,7 +8053,6 @@ void main() {
controller.selection, controller.selection,
equals(const TextSelection.collapsed( equals(const TextSelection.collapsed(
offset: testText.length, offset: testText.length,
affinity: TextAffinity.upstream,
)), )),
); );
...@@ -11036,7 +11010,6 @@ void main() { ...@@ -11036,7 +11010,6 @@ void main() {
controller.selection = const TextSelection( controller.selection = const TextSelection(
baseOffset: 15, baseOffset: 15,
extentOffset: 15, extentOffset: 15,
affinity: TextAffinity.upstream,
); );
await tester.pump(); await tester.pump();
expect(controller.selection.isCollapsed, true); expect(controller.selection.isCollapsed, true);
...@@ -11275,7 +11248,6 @@ void main() { ...@@ -11275,7 +11248,6 @@ void main() {
const TextSelection( const TextSelection(
baseOffset: 29, baseOffset: 29,
extentOffset: 0, extentOffset: 0,
affinity: TextAffinity.upstream,
), ),
), ),
); );
......
...@@ -1296,12 +1296,12 @@ void main() { ...@@ -1296,12 +1296,12 @@ void main() {
await sendKeyCombination(tester, SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, alt: alt, control: control)); await sendKeyCombination(tester, SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, alt: alt, control: control));
await tester.pump(); await tester.pump();
// Ho[w are you]? // Ho[w are ]you?
// Good, and you? // Good, and you?
// Fine, thank you. // Fine, thank you.
expect(paragraph1.selections.length, 1); expect(paragraph1.selections.length, 1);
expect(paragraph1.selections[0].start, 2); expect(paragraph1.selections[0].start, 2);
expect(paragraph1.selections[0].end, 11); expect(paragraph1.selections[0].end, 8);
expect(paragraph2.selections.length, 0); expect(paragraph2.selections.length, 0);
}, variant: TargetPlatformVariant.all()); }, variant: TargetPlatformVariant.all());
......
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