Makes TextBoundary and its subclasses public (#110367)

This commit is contained in:
chunhtai 2022-09-16 15:05:04 -07:00 committed by GitHub
parent a7f028f170
commit bcc1dc6b99
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 530 additions and 302 deletions

View file

@ -43,6 +43,7 @@ export 'src/services/system_channels.dart';
export 'src/services/system_chrome.dart';
export 'src/services/system_navigator.dart';
export 'src/services/system_sound.dart';
export 'src/services/text_boundary.dart';
export 'src/services/text_editing.dart';
export 'src/services/text_editing_delta.dart';
export 'src/services/text_formatter.dart';

View file

@ -1129,6 +1129,12 @@ class TextPainter {
/// {@endtemplate}
TextRange getWordBoundary(TextPosition position) {
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);
}

View file

@ -2040,7 +2040,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
final TextSelection firstWord = _getWordAtOffset(firstPosition);
final TextSelection lastWord = to == null ?
firstWord : _getWordAtOffset(_textPainter.getPositionForOffset(globalToLocal(to - _paintOffset)));
_setSelection(
TextSelection(
baseOffset: firstWord.base.offset,
@ -2071,14 +2070,28 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
TextSelection _getWordAtOffset(TextPosition position) {
debugAssertLayoutUpToDate();
final TextRange word = _textPainter.getWordBoundary(position);
// When long-pressing past the end of the text, we want a collapsed cursor.
if (position.offset >= word.end) {
return TextSelection.fromPosition(position);
if (position.offset >= _plainText.length) {
return TextSelection.fromPosition(
TextPosition(offset: _plainText.length, affinity: TextAffinity.upstream)
);
}
// If text is obscured, the entire sentence should be treated as one word.
if (obscureText) {
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
// 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.
@ -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
// previous word if there is one; otherwise, select the single whitespace at
// the position.
} else if (TextLayoutMetrics.isWhitespace(_plainText.codeUnitAt(position.offset))
&& position.offset > 0) {
if (TextLayoutMetrics.isWhitespace(_plainText.codeUnitAt(effectiveOffset))
&& effectiveOffset > 0) {
assert(defaultTargetPlatform != null);
final TextRange? previousWord = _getPreviousWord(word.start);
switch (defaultTargetPlatform) {

View file

@ -0,0 +1,179 @@
// 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,
);
}
}

View file

@ -3478,23 +3478,23 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
// --------------------------- Text Editing Actions ---------------------------
_TextBoundary _characterBoundary(DirectionalTextEditingIntent intent) {
final _TextBoundary atomicTextBoundary = widget.obscureText ? _CodeUnitBoundary(_value) : _CharacterBoundary(_value);
return _CollapsedSelectionBoundary(atomicTextBoundary, intent.forward);
TextBoundary _characterBoundary(DirectionalTextEditingIntent intent) {
final TextBoundary atomicTextBoundary = widget.obscureText ? _CodeUnitBoundary(_value.text) : CharacterBoundary(_value.text);
return _PushTextPosition(atomicTextBoundary, intent.forward);
}
_TextBoundary _nextWordBoundary(DirectionalTextEditingIntent intent) {
final _TextBoundary atomicTextBoundary;
final _TextBoundary boundary;
TextBoundary _nextWordBoundary(DirectionalTextEditingIntent intent) {
final TextBoundary atomicTextBoundary;
final TextBoundary boundary;
if (widget.obscureText) {
atomicTextBoundary = _CodeUnitBoundary(_value);
boundary = _DocumentBoundary(_value);
atomicTextBoundary = _CodeUnitBoundary(_value.text);
boundary = DocumentBoundary(_value.text);
} else {
final TextEditingValue textEditingValue = _textEditingValueforTextLayoutMetrics;
atomicTextBoundary = _CharacterBoundary(textEditingValue);
atomicTextBoundary = CharacterBoundary(textEditingValue.text);
// This isn't enough. Newline characters.
boundary = _ExpandedTextBoundary(_WhitespaceBoundary(textEditingValue), _WordBoundary(renderEditable, textEditingValue));
boundary = _ExpandedTextBoundary(_WhitespaceBoundary(textEditingValue.text), WordBoundary(renderEditable));
}
final _MixedBoundary mixedBoundary = intent.forward
@ -3502,20 +3502,20 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
: _MixedBoundary(boundary, atomicTextBoundary);
// Use a _MixedBoundary to make sure we don't leave invalid codepoints in
// the field after deletion.
return _CollapsedSelectionBoundary(mixedBoundary, intent.forward);
return _PushTextPosition(mixedBoundary, intent.forward);
}
_TextBoundary _linebreak(DirectionalTextEditingIntent intent) {
final _TextBoundary atomicTextBoundary;
final _TextBoundary boundary;
TextBoundary _linebreak(DirectionalTextEditingIntent intent) {
final TextBoundary atomicTextBoundary;
final TextBoundary boundary;
if (widget.obscureText) {
atomicTextBoundary = _CodeUnitBoundary(_value);
boundary = _DocumentBoundary(_value);
atomicTextBoundary = _CodeUnitBoundary(_value.text);
boundary = DocumentBoundary(_value.text);
} else {
final TextEditingValue textEditingValue = _textEditingValueforTextLayoutMetrics;
atomicTextBoundary = _CharacterBoundary(textEditingValue);
boundary = _LineBreak(renderEditable, textEditingValue);
atomicTextBoundary = CharacterBoundary(textEditingValue.text);
boundary = LineBreak(renderEditable);
}
// The _MixedBoundary is to make sure we don't leave invalid code units in
@ -3524,11 +3524,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
// since the document boundary is unique and the linebreak boundary is
// already caret-location based.
return intent.forward
? _MixedBoundary(_CollapsedSelectionBoundary(atomicTextBoundary, true), boundary)
: _MixedBoundary(boundary, _CollapsedSelectionBoundary(atomicTextBoundary, false));
? _MixedBoundary(_PushTextPosition(atomicTextBoundary, true), boundary)
: _MixedBoundary(boundary, _PushTextPosition(atomicTextBoundary, false));
}
_TextBoundary _documentBoundary(DirectionalTextEditingIntent intent) => _DocumentBoundary(_value);
TextBoundary _documentBoundary(DirectionalTextEditingIntent intent) => DocumentBoundary(_value.text);
Action<T> _makeOverridable<T extends Intent>(Action<T> defaultAction) {
return Action<T>.overridable(context: context, defaultAction: defaultAction);
@ -3615,17 +3615,17 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
late final _UpdateTextSelectionToAdjacentLineAction<ExtendSelectionVerticallyToAdjacentLineIntent> _adjacentLineAction = _UpdateTextSelectionToAdjacentLineAction<ExtendSelectionVerticallyToAdjacentLineIntent>(this);
void _expandSelectionToDocumentBoundary(ExpandSelectionToDocumentBoundaryIntent intent) {
final _TextBoundary textBoundary = _documentBoundary(intent);
final TextBoundary textBoundary = _documentBoundary(intent);
_expandSelection(intent.forward, textBoundary, true);
}
void _expandSelectionToLinebreak(ExpandSelectionToLineBreakIntent intent) {
final _TextBoundary textBoundary = _linebreak(intent);
final TextBoundary textBoundary = _linebreak(intent);
_expandSelection(intent.forward, textBoundary);
}
void _expandSelection(bool forward, _TextBoundary textBoundary, [bool extentAtIndex = false]) {
final TextSelection textBoundarySelection = textBoundary.textEditingValue.selection;
void _expandSelection(bool forward, TextBoundary textBoundary, [bool extentAtIndex = false]) {
final TextSelection textBoundarySelection = _value.selection;
if (!textBoundarySelection.isValid) {
return;
}
@ -4220,71 +4220,84 @@ class _ScribblePlaceholder extends WidgetSpan {
}
}
/// An interface for retrieving the logical text boundary (left-closed-right-open)
/// at a given location in a document.
/// A text boundary that uses code units as logical boundaries.
///
/// Depending on the implementation of the [_TextBoundary], the input
/// [TextPosition] can either point to a code unit, or a position between 2 code
/// units (which can be visually represented by the caret if the selection were
/// to collapse to that position).
///
/// For example, [_LineBreak] interprets the input [TextPosition] as a caret
/// location, since in Flutter the caret is generally painted between the
/// character the [TextPosition] points to and its previous character, and
/// [_LineBreak] cares about the affinity of the input [TextPosition]. Most
/// other text boundaries however, interpret the input [TextPosition] as the
/// location of a code unit in the document, since it's easier to reason about
/// the text boundary given a code unit in the text.
///
/// To convert a "code-unit-based" [_TextBoundary] to "caret-location-based",
/// use the [_CollapsedSelectionBoundary] combinator.
abstract class _TextBoundary {
const _TextBoundary();
/// This text boundary treats every character in input string as an utf-16 code
/// unit. This can be useful when handling text without any grapheme cluster,
/// e.g. the obscure string in [EditableText]. If you are handling text that may
/// include grapheme clusters, consider using [CharacterBoundary].
class _CodeUnitBoundary extends TextBoundary {
const _CodeUnitBoundary(this._text);
TextEditingValue get textEditingValue;
/// Returns the leading text boundary at the given location, inclusive.
TextPosition getLeadingTextBoundaryAt(TextPosition position);
/// Returns the trailing text boundary at the given location, exclusive.
TextPosition getTrailingTextBoundaryAt(TextPosition position);
TextRange getTextBoundaryAt(TextPosition position) {
return TextRange(
start: getLeadingTextBoundaryAt(position).offset,
end: getTrailingTextBoundaryAt(position).offset,
);
}
}
// ----------------------------- Text Boundaries -----------------------------
class _CodeUnitBoundary extends _TextBoundary {
const _CodeUnitBoundary(this.textEditingValue);
@override
final TextEditingValue textEditingValue;
@override
TextPosition getLeadingTextBoundaryAt(TextPosition position) => TextPosition(offset: position.offset);
@override
TextPosition getTrailingTextBoundaryAt(TextPosition position) => TextPosition(offset: math.min(position.offset + 1, textEditingValue.text.length));
}
// The word modifier generally removes the word boundaries around white spaces
// (and newlines), IOW white spaces and some other punctuations are considered
// a part of the next word in the search direction.
class _WhitespaceBoundary extends _TextBoundary {
const _WhitespaceBoundary(this.textEditingValue);
@override
final TextEditingValue textEditingValue;
final String _text;
@override
TextPosition getLeadingTextBoundaryAt(TextPosition position) {
for (int index = position.offset; index >= 0; index -= 1) {
if (!TextLayoutMetrics.isWhitespace(textEditingValue.text.codeUnitAt(index))) {
return TextPosition(offset: index);
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);
}
switch (position.affinity) {
case TextAffinity.upstream:
return TextPosition(offset: math.min(position.offset - 1, _text.length));
case TextAffinity.downstream:
return TextPosition(offset: math.min(position.offset, _text.length));
}
}
@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);
}
switch (position.affinity) {
case TextAffinity.upstream:
return TextPosition(offset: math.min(position.offset, _text.length), affinity: TextAffinity.upstream);
case TextAffinity.downstream:
return TextPosition(offset: math.min(position.offset + 1, _text.length), affinity: TextAffinity.upstream);
}
}
}
// ------------------------ Text Boundary Combinators ------------------------
/// A text boundary that use the first non-whitespace character as the logical
/// boundary.
///
/// This text boundary uses [TextLayoutMetrics.isWhitespace] to identify white
/// spaces, this include newline characters from ASCII and separators from the
/// [unicode separator category](https://www.compart.com/en/unicode/category/Zs).
class _WhitespaceBoundary extends TextBoundary {
/// Creates a [_WhitespaceBoundary] with the text.
const _WhitespaceBoundary(this._text);
final String _text;
@override
TextPosition getLeadingTextBoundaryAt(TextPosition position) {
// Handles outside of right bound.
if (position.offset > _text.length || (position.offset == _text.length && position.affinity == TextAffinity.downstream)) {
position = TextPosition(offset: _text.length, affinity: TextAffinity.upstream);
}
// Handles outside of left bound.
if (position.offset <= 0) {
return const TextPosition(offset: 0);
}
int index = position.offset;
if (!TextLayoutMetrics.isWhitespace(_text.codeUnitAt(index)) && position.affinity == TextAffinity.downstream) {
return position;
}
for (index -= 1; index >= 0; index -= 1) {
if (!TextLayoutMetrics.isWhitespace(_text.codeUnitAt(index))) {
return TextPosition(offset: index + 1, affinity: TextAffinity.upstream);
}
}
return const TextPosition(offset: 0);
@ -4292,142 +4305,35 @@ class _WhitespaceBoundary extends _TextBoundary {
@override
TextPosition getTrailingTextBoundaryAt(TextPosition position) {
for (int index = position.offset; index < textEditingValue.text.length; index += 1) {
if (!TextLayoutMetrics.isWhitespace(textEditingValue.text.codeUnitAt(index))) {
return TextPosition(offset: index + 1);
// Handles outside of right bound.
if (position.offset >= _text.length) {
return TextPosition(offset: _text.length, affinity: TextAffinity.upstream);
}
// Handles outside of left bound.
if (position.offset < 0 || (position.offset == 0 && position.affinity == TextAffinity.upstream)) {
position = const TextPosition(offset: 0);
}
int index = position.offset;
if (!TextLayoutMetrics.isWhitespace(_text.codeUnitAt(index)) && position.affinity == TextAffinity.downstream) {
return position;
}
for (index += 1; index < _text.length; index += 1) {
if (!TextLayoutMetrics.isWhitespace(_text.codeUnitAt(index))) {
return TextPosition(offset: index);
}
}
return TextPosition(offset: textEditingValue.text.length);
return TextPosition(offset: _text.length, affinity: TextAffinity.upstream);
}
}
// Most apps delete the entire grapheme when the backspace key is pressed.
// Also always put the new caret location to character boundaries to avoid
// sending malformed UTF-16 code units to the paragraph builder.
class _CharacterBoundary extends _TextBoundary {
const _CharacterBoundary(this.textEditingValue);
@override
final TextEditingValue textEditingValue;
@override
TextPosition getLeadingTextBoundaryAt(TextPosition position) {
final int endOffset = math.min(position.offset + 1, textEditingValue.text.length);
return TextPosition(
offset: CharacterRange.at(textEditingValue.text, position.offset, endOffset).stringBeforeLength,
);
}
@override
TextPosition getTrailingTextBoundaryAt(TextPosition position) {
final int endOffset = math.min(position.offset + 1, textEditingValue.text.length);
final CharacterRange range = CharacterRange.at(textEditingValue.text, position.offset, endOffset);
return TextPosition(
offset: textEditingValue.text.length - range.stringAfterLength,
);
}
@override
TextRange getTextBoundaryAt(TextPosition position) {
final int endOffset = math.min(position.offset + 1, textEditingValue.text.length);
final CharacterRange range = CharacterRange.at(textEditingValue.text, position.offset, endOffset);
return TextRange(
start: range.stringBeforeLength,
end: textEditingValue.text.length - range.stringAfterLength,
);
}
}
// [UAX #29](https://unicode.org/reports/tr29/) defined word boundaries.
class _WordBoundary extends _TextBoundary {
const _WordBoundary(this.textLayout, this.textEditingValue);
final TextLayoutMetrics textLayout;
@override
final TextEditingValue textEditingValue;
@override
TextPosition getLeadingTextBoundaryAt(TextPosition position) {
return TextPosition(
offset: textLayout.getWordBoundary(position).start,
// Word boundary seems to always report downstream on many platforms.
affinity: TextAffinity.downstream, // ignore: avoid_redundant_argument_values
);
}
@override
TextPosition getTrailingTextBoundaryAt(TextPosition position) {
return TextPosition(
offset: textLayout.getWordBoundary(position).end,
// Word boundary seems to always report downstream on many platforms.
affinity: TextAffinity.downstream, // ignore: avoid_redundant_argument_values
);
}
}
// The linebreaks of the current text layout. The input [TextPosition]s are
// interpreted as caret locations because [TextPainter.getLineAtOffset] is
// text-affinity-aware.
class _LineBreak extends _TextBoundary {
const _LineBreak(
this.textLayout,
this.textEditingValue,
);
final TextLayoutMetrics textLayout;
@override
final TextEditingValue textEditingValue;
@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,
);
}
}
// The document boundary is unique and is a constant function of the input
// position.
class _DocumentBoundary extends _TextBoundary {
const _DocumentBoundary(this.textEditingValue);
@override
final TextEditingValue textEditingValue;
@override
TextPosition getLeadingTextBoundaryAt(TextPosition position) => const TextPosition(offset: 0);
@override
TextPosition getTrailingTextBoundaryAt(TextPosition position) {
return TextPosition(
offset: textEditingValue.text.length,
affinity: TextAffinity.upstream,
);
}
}
// ------------------------ Text Boundary Combinators ------------------------
// Expands the innerTextBoundary with outerTextBoundary.
class _ExpandedTextBoundary extends _TextBoundary {
class _ExpandedTextBoundary extends TextBoundary {
_ExpandedTextBoundary(this.innerTextBoundary, this.outerTextBoundary);
final _TextBoundary innerTextBoundary;
final _TextBoundary outerTextBoundary;
@override
TextEditingValue get textEditingValue {
assert(innerTextBoundary.textEditingValue == outerTextBoundary.textEditingValue);
return innerTextBoundary.textEditingValue;
}
final TextBoundary innerTextBoundary;
final TextBoundary outerTextBoundary;
@override
TextPosition getLeadingTextBoundaryAt(TextPosition position) {
@ -4444,49 +4350,70 @@ class _ExpandedTextBoundary extends _TextBoundary {
}
}
// Force the innerTextBoundary to interpret the input [TextPosition]s as caret
// locations instead of code unit positions.
//
// The innerTextBoundary must be a [_TextBoundary] that interprets the input
// [TextPosition]s as code unit positions.
class _CollapsedSelectionBoundary extends _TextBoundary {
_CollapsedSelectionBoundary(this.innerTextBoundary, this.isForward);
/// A proxy text boundary that will push input text position forward or backward
/// one affinity unit before sending it to the [innerTextBoundary].
///
/// If the [isForward] is true, this proxy text boundary push the position
/// forward; otherwise, backward.
///
/// To push a text position forward one affinity unit, this proxy converts
/// affinity to downstream if it is upstream; otherwise it increase the offset
/// by one with its affinity sets to upstream. For example,
/// `TextPosition(1, upstream)` becomes `TextPosition(1, downstream)`,
/// `TextPosition(4, downstream)` becomes `TextPosition(5, upstream)`.
///
/// This class is used to kick-start the text position to find the next boundary
/// determined by [innerTextBoundary] so that it won't be trapped if the input
/// text position is right at the edge.
class _PushTextPosition extends TextBoundary {
_PushTextPosition(this.innerTextBoundary, this.isForward);
final _TextBoundary innerTextBoundary;
final TextBoundary innerTextBoundary;
final bool isForward;
@override
TextEditingValue get textEditingValue => innerTextBoundary.textEditingValue;
TextPosition _calculateTargetPosition(TextPosition position) {
if (isForward) {
switch(position.affinity) {
case TextAffinity.upstream:
return TextPosition(offset: position.offset);
case TextAffinity.downstream:
return position = TextPosition(
offset: position.offset + 1,
affinity: TextAffinity.upstream,
);
}
} else {
switch(position.affinity) {
case TextAffinity.upstream:
return position = TextPosition(offset: position.offset - 1);
case TextAffinity.downstream:
return TextPosition(
offset: position.offset,
affinity: TextAffinity.upstream,
);
}
}
}
@override
TextPosition getLeadingTextBoundaryAt(TextPosition position) {
return isForward
? innerTextBoundary.getLeadingTextBoundaryAt(position)
: position.offset <= 0 ? const TextPosition(offset: 0) : innerTextBoundary.getLeadingTextBoundaryAt(TextPosition(offset: position.offset - 1));
return innerTextBoundary.getLeadingTextBoundaryAt(_calculateTargetPosition(position));
}
@override
TextPosition getTrailingTextBoundaryAt(TextPosition position) {
return isForward
? innerTextBoundary.getTrailingTextBoundaryAt(position)
: position.offset <= 0 ? const TextPosition(offset: 0) : innerTextBoundary.getTrailingTextBoundaryAt(TextPosition(offset: position.offset - 1));
return innerTextBoundary.getTrailingTextBoundaryAt(_calculateTargetPosition(position));
}
}
// A _TextBoundary that creates a [TextRange] where its start is from the
// specified leading text boundary and its end is from the specified trailing
// text boundary.
class _MixedBoundary extends _TextBoundary {
class _MixedBoundary extends TextBoundary {
_MixedBoundary(this.leadingTextBoundary, this.trailingTextBoundary);
final _TextBoundary leadingTextBoundary;
final _TextBoundary trailingTextBoundary;
@override
TextEditingValue get textEditingValue {
assert(leadingTextBoundary.textEditingValue == trailingTextBoundary.textEditingValue);
return leadingTextBoundary.textEditingValue;
}
final TextBoundary leadingTextBoundary;
final TextBoundary trailingTextBoundary;
@override
TextPosition getLeadingTextBoundaryAt(TextPosition position) => leadingTextBoundary.getLeadingTextBoundaryAt(position);
@ -4500,15 +4427,15 @@ class _DeleteTextAction<T extends DirectionalTextEditingIntent> extends ContextA
_DeleteTextAction(this.state, this.getTextBoundariesForIntent);
final EditableTextState state;
final _TextBoundary Function(T intent) getTextBoundariesForIntent;
final TextBoundary Function(T intent) getTextBoundariesForIntent;
TextRange _expandNonCollapsedRange(TextEditingValue value) {
final TextRange selection = value.selection;
assert(selection.isValid);
assert(!selection.isCollapsed);
final _TextBoundary atomicBoundary = state.widget.obscureText
? _CodeUnitBoundary(value)
: _CharacterBoundary(value);
final TextBoundary atomicBoundary = state.widget.obscureText
? _CodeUnitBoundary(value.text)
: CharacterBoundary(value.text);
return TextRange(
start: atomicBoundary.getLeadingTextBoundaryAt(TextPosition(offset: selection.start)).offset,
@ -4528,23 +4455,23 @@ class _DeleteTextAction<T extends DirectionalTextEditingIntent> extends ContextA
);
}
final _TextBoundary textBoundary = getTextBoundariesForIntent(intent);
if (!textBoundary.textEditingValue.selection.isValid) {
final TextBoundary textBoundary = getTextBoundariesForIntent(intent);
if (!state._value.selection.isValid) {
return null;
}
if (!textBoundary.textEditingValue.selection.isCollapsed) {
if (!state._value.selection.isCollapsed) {
return Actions.invoke(
context!,
ReplaceTextIntent(state._value, '', _expandNonCollapsedRange(textBoundary.textEditingValue), SelectionChangedCause.keyboard),
ReplaceTextIntent(state._value, '', _expandNonCollapsedRange(state._value), SelectionChangedCause.keyboard),
);
}
return Actions.invoke(
context!,
ReplaceTextIntent(
textBoundary.textEditingValue,
state._value,
'',
textBoundary.getTextBoundaryAt(textBoundary.textEditingValue.selection.base),
textBoundary.getTextBoundaryAt(state._value.selection.base),
SelectionChangedCause.keyboard,
),
);
@ -4563,7 +4490,7 @@ class _UpdateTextSelectionAction<T extends DirectionalCaretMovementIntent> exten
final EditableTextState state;
final bool ignoreNonCollapsedSelection;
final _TextBoundary Function(T intent) getTextBoundariesForIntent;
final TextBoundary Function(T intent) getTextBoundariesForIntent;
static const int NEWLINE_CODE_UNIT = 10;
@ -4578,7 +4505,7 @@ class _UpdateTextSelectionAction<T extends DirectionalCaretMovementIntent> exten
&& state.textEditingValue.text.codeUnitAt(position.offset) != NEWLINE_CODE_UNIT;
}
// Returns true iff the given position at a wordwrap boundary in the
// Returns true if the given position at a wordwrap boundary in the
// downstream position.
bool _isAtWordwrapDownstream(TextPosition position) {
final TextPosition start = TextPosition(
@ -4611,29 +4538,9 @@ class _UpdateTextSelectionAction<T extends DirectionalCaretMovementIntent> exten
);
}
final _TextBoundary textBoundary = getTextBoundariesForIntent(intent);
// "textBoundary's selection is only updated after rebuild; if the text
// is the same, use the selection from state, which is more recent.
// This is necessary on macOS where alt+up sends the moveBackward:
// and moveToBeginningOfParagraph: selectors at the same time.
final TextSelection textBoundarySelection =
textBoundary.textEditingValue.text == state._value.text
? state._value.selection
: textBoundary.textEditingValue.selection;
if (!textBoundarySelection.isValid) {
return null;
}
if (!textBoundarySelection.isCollapsed && !ignoreNonCollapsedSelection && collapseSelection) {
return Actions.invoke(
context!,
UpdateSelectionIntent(state._value, collapse(textBoundarySelection), SelectionChangedCause.keyboard),
);
}
TextPosition extent = textBoundarySelection.extent;
final TextBoundary textBoundary = getTextBoundariesForIntent(intent);
TextPosition extent = selection.extent;
// If continuesAtWrap is true extent and is at the relevant wordwrap, then
// move it just to the other side of the wordwrap.
if (intent.continuesAtWrap) {
@ -4652,10 +4559,9 @@ class _UpdateTextSelectionAction<T extends DirectionalCaretMovementIntent> exten
final TextPosition newExtent = intent.forward
? textBoundary.getTrailingTextBoundaryAt(extent)
: textBoundary.getLeadingTextBoundaryAt(extent);
final TextSelection newSelection = collapseSelection
? TextSelection.fromPosition(newExtent)
: textBoundarySelection.extendTo(newExtent);
: selection.extendTo(newExtent);
// If collapseAtReversal is true and would have an effect, collapse it.
if (!selection.isCollapsed && intent.collapseAtReversal
@ -4673,7 +4579,7 @@ class _UpdateTextSelectionAction<T extends DirectionalCaretMovementIntent> exten
return Actions.invoke(
context!,
UpdateSelectionIntent(textBoundary.textEditingValue, newSelection, SelectionChangedCause.keyboard),
UpdateSelectionIntent(state._value, newSelection, SelectionChangedCause.keyboard),
);
}
@ -4685,15 +4591,15 @@ class _ExtendSelectionOrCaretPositionAction extends ContextAction<ExtendSelectio
_ExtendSelectionOrCaretPositionAction(this.state, this.getTextBoundariesForIntent);
final EditableTextState state;
final _TextBoundary Function(ExtendSelectionToNextWordBoundaryOrCaretLocationIntent intent) getTextBoundariesForIntent;
final TextBoundary Function(ExtendSelectionToNextWordBoundaryOrCaretLocationIntent intent) getTextBoundariesForIntent;
@override
Object? invoke(ExtendSelectionToNextWordBoundaryOrCaretLocationIntent intent, [BuildContext? context]) {
final TextSelection selection = state._value.selection;
assert(selection.isValid);
final _TextBoundary textBoundary = getTextBoundariesForIntent(intent);
final TextSelection textBoundarySelection = textBoundary.textEditingValue.selection;
final TextBoundary textBoundary = getTextBoundariesForIntent(intent);
final TextSelection textBoundarySelection = state._value.selection;
if (!textBoundarySelection.isValid) {
return null;
}
@ -4712,7 +4618,7 @@ class _ExtendSelectionOrCaretPositionAction extends ContextAction<ExtendSelectio
return Actions.invoke(
context!,
UpdateSelectionIntent(textBoundary.textEditingValue, newSelection, SelectionChangedCause.keyboard),
UpdateSelectionIntent(state._value, newSelection, SelectionChangedCause.keyboard),
);
}

View file

@ -199,7 +199,8 @@ void main() {
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 {
EditableText.debugDeterministicCursor = false;
@ -2087,6 +2088,7 @@ void main() {
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(textOffsetToPosition(tester, 5));
await tester.pumpAndSettle();
expect(controller.value.selection, isNotNull);
expect(controller.value.selection.baseOffset, 5);
expect(controller.value.selection.extentOffset, 6);
@ -3053,7 +3055,7 @@ void main() {
expect(controller.selection.extentOffset, 8);
// 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();
expect(selectionChangedCount, 0);

View file

@ -2172,7 +2172,7 @@ void main() {
expect(controller.selection.extentOffset, 8);
// 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();
expect(selectionChangedCount, 0);
@ -3372,10 +3372,7 @@ void main() {
final Offset secondPos = textOffsetToPosition(tester, testValue.indexOf('Second'));
final Offset thirdPos = textOffsetToPosition(tester, testValue.indexOf('Third'));
final Offset middleStringPos = textOffsetToPosition(tester, testValue.indexOf('irst'));
expect(firstPos.dx, 0);
expect(secondPos.dx, 0);
expect(thirdPos.dx, 0);
expect(middleStringPos.dx, 34);
expect(firstPos.dx, lessThan(middleStringPos.dx));
expect(firstPos.dx, secondPos.dx);
expect(firstPos.dx, thirdPos.dx);
expect(firstPos.dy, lessThan(secondPos.dy));
@ -3457,8 +3454,6 @@ void main() {
// Check that the last line of text is not displayed.
final Offset firstPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First'));
final Offset fourthPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('Fourth'));
expect(firstPos.dx, 0);
expect(fourthPos.dx, 0);
expect(firstPos.dx, fourthPos.dx);
expect(firstPos.dy, lessThan(fourthPos.dy));
expect(inputBox.hitTest(BoxHitTestResult(), position: inputBox.globalToLocal(firstPos)), isTrue);
@ -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
// the right side of the screen.
expect(textOffsetToPosition(tester, 66).dx, 1056);
expect(textOffsetToPosition(tester, 66).dx, greaterThan(screenSize.width));
final TestGesture gesture =
await tester.startGesture(
@ -8448,7 +8443,7 @@ void main() {
);
// 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());
testWidgets('keyboard selection change scrolls the field', (WidgetTester tester) async {
@ -8485,7 +8480,7 @@ void main() {
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection.collapsed(offset: 56),
const TextSelection.collapsed(offset: 56, affinity: TextAffinity.upstream),
);
// Keep moving out.
@ -8495,7 +8490,7 @@ void main() {
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection.collapsed(offset: 62),
const TextSelection.collapsed(offset: 62, affinity: TextAffinity.upstream),
);
for (int i = 0; i < (66 - 62); i += 1) {
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
@ -8503,7 +8498,7 @@ void main() {
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection.collapsed(offset: 66),
const TextSelection.collapsed(offset: 66, affinity: TextAffinity.upstream),
); // We're at the edge now.
await tester.pumpAndSettle();

View file

@ -0,0 +1,97 @@
// 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();
}
}

View file

@ -1229,6 +1229,7 @@ void main() {
expect(controller.selection, const TextSelection.collapsed(
offset: 21,
affinity: TextAffinity.upstream,
));
}, variant: TargetPlatformVariant.all());
@ -1243,6 +1244,7 @@ void main() {
expect(controller.selection, const TextSelection.collapsed(
offset: 10,
affinity: TextAffinity.upstream,
));
}, variant: allExceptApple);
@ -1353,6 +1355,7 @@ void main() {
await tester.pump();
expect(controller.selection, const TextSelection.collapsed(
offset: 46, // After "to".
affinity: TextAffinity.upstream,
));
// "good" to "come" is selected.
@ -1365,6 +1368,7 @@ void main() {
await tester.pump();
expect(controller.selection, const TextSelection.collapsed(
offset: 28, // After "good".
affinity: TextAffinity.upstream,
));
}, variant: allExceptApple);
@ -1673,6 +1677,7 @@ void main() {
expect(controller.selection, const TextSelection.collapsed(
offset: 10,
affinity: TextAffinity.upstream,
));
}, variant: macOSOnly);
@ -1743,6 +1748,7 @@ void main() {
await tester.pump();
expect(controller.selection, const TextSelection.collapsed(
offset: 46, // After "to".
affinity: TextAffinity.upstream,
));
// "good" to "come" is selected.
@ -1755,6 +1761,7 @@ void main() {
await tester.pump();
expect(controller.selection, const TextSelection.collapsed(
offset: 28, // After "good".
affinity: TextAffinity.upstream,
));
}, variant: macOSOnly);

View file

@ -5832,6 +5832,7 @@ void main() {
equals(
const TextSelection.collapsed(
offset: 3,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
@ -5941,6 +5942,7 @@ void main() {
const TextSelection(
baseOffset: 10,
extentOffset: 10,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
@ -6398,6 +6400,7 @@ void main() {
equals(
const TextSelection.collapsed(
offset: 23,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
@ -6422,6 +6425,7 @@ void main() {
equals(
const TextSelection.collapsed(
offset: 23,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
@ -6464,6 +6468,7 @@ void main() {
equals(
const TextSelection.collapsed(
offset: 23,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
@ -6549,6 +6554,7 @@ void main() {
equals(
const TextSelection.collapsed(
offset: 32,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
@ -6573,6 +6579,7 @@ void main() {
equals(
const TextSelection.collapsed(
offset: 32,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
@ -6615,6 +6622,7 @@ void main() {
equals(
const TextSelection.collapsed(
offset: 32,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
@ -6710,6 +6718,7 @@ void main() {
equals(
const TextSelection.collapsed(
offset: 32,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
@ -6734,6 +6743,7 @@ void main() {
equals(
const TextSelection.collapsed(
offset: 32,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
@ -6776,6 +6786,7 @@ void main() {
equals(
const TextSelection.collapsed(
offset: 32,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
@ -6872,6 +6883,7 @@ void main() {
equals(
const TextSelection.collapsed(
offset: 23,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
@ -6917,6 +6929,7 @@ void main() {
const TextSelection(
baseOffset: 23,
extentOffset: 23,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
@ -6927,6 +6940,7 @@ void main() {
const TextSelection(
baseOffset: 23,
extentOffset: 23,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
@ -7060,6 +7074,7 @@ void main() {
controller.selection,
equals(const TextSelection.collapsed(
offset: 4,
affinity: TextAffinity.upstream,
)),
);
@ -7243,6 +7258,7 @@ void main() {
equals(
const TextSelection.collapsed(
offset: 32,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
@ -7266,6 +7282,7 @@ void main() {
equals(
const TextSelection.collapsed(
offset: 32,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
@ -7323,6 +7340,7 @@ void main() {
equals(
const TextSelection.collapsed(
offset: 32,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
@ -7435,6 +7453,7 @@ void main() {
equals(
const TextSelection.collapsed(
offset: 32,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
@ -7458,6 +7477,7 @@ void main() {
equals(
const TextSelection.collapsed(
offset: 32,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
@ -7515,6 +7535,7 @@ void main() {
equals(
const TextSelection.collapsed(
offset: 32,
affinity: TextAffinity.upstream,
),
),
reason: 'on $platform',
@ -7626,6 +7647,7 @@ void main() {
equals(
const TextSelection.collapsed(
offset: 32,
affinity: TextAffinity.upstream,
),
),
);
@ -10383,7 +10405,6 @@ void main() {
expect(controller.selection.isCollapsed, false);
expect(controller.selection.baseOffset, 7);
expect(controller.selection.extentOffset, 10);
await sendKeys(
tester,
<LogicalKeyboardKey>[LogicalKeyboardKey.arrowLeft],

View file

@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
@ -42,7 +43,7 @@ Offset textOffsetToPosition(WidgetTester tester, int offset) {
renderEditable,
);
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.