diff --git a/packages/flutter/lib/src/rendering/editable.dart b/packages/flutter/lib/src/rendering/editable.dart index 07853bb918..589fad3e8d 100644 --- a/packages/flutter/lib/src/rendering/editable.dart +++ b/packages/flutter/lib/src/rendering/editable.dart @@ -2067,9 +2067,9 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, void selectWordsInRange({ required Offset from, Offset? to, required SelectionChangedCause cause }) { _computeTextMetricsIfNeeded(); final TextPosition fromPosition = _textPainter.getPositionForOffset(globalToLocal(from - _paintOffset)); - final TextSelection fromWord = _getWordAtOffset(fromPosition); + final TextSelection fromWord = getWordAtOffset(fromPosition); final TextPosition toPosition = to == null ? fromPosition : _textPainter.getPositionForOffset(globalToLocal(to - _paintOffset)); - final TextSelection toWord = toPosition == fromPosition ? fromWord : _getWordAtOffset(toPosition); + final TextSelection toWord = toPosition == fromPosition ? fromWord : getWordAtOffset(toPosition); final bool isFromWordBeforeToWord = fromWord.start < toWord.end; _setSelection( @@ -2099,7 +2099,10 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, _setSelection(newSelection, cause); } - TextSelection _getWordAtOffset(TextPosition position) { + /// Returns a [TextSelection] that encompasses the word at the given + /// [TextPosition]. + @visibleForTesting + TextSelection getWordAtOffset(TextPosition position) { debugAssertLayoutUpToDate(); // When long-pressing past the end of the text, we want a collapsed cursor. if (position.offset >= plainText.length) { @@ -2120,6 +2123,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, case TextAffinity.downstream: effectiveOffset = position.offset; } + assert(effectiveOffset >= 0); // 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 @@ -2128,8 +2132,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. - if (TextLayoutMetrics.isWhitespace(plainText.codeUnitAt(effectiveOffset)) - && effectiveOffset > 0) { + if (effectiveOffset > 0 + && TextLayoutMetrics.isWhitespace(plainText.codeUnitAt(effectiveOffset))) { final TextRange? previousWord = _getPreviousWord(word.start); switch (defaultTargetPlatform) { case TargetPlatform.iOS: diff --git a/packages/flutter/test/rendering/editable_test.dart b/packages/flutter/test/rendering/editable_test.dart index 6f2f74afdc..0a31aa54e1 100644 --- a/packages/flutter/test/rendering/editable_test.dart +++ b/packages/flutter/test/rendering/editable_test.dart @@ -1823,6 +1823,52 @@ void main() { rrect: expectedRRect )); }); + + test('getWordAtOffset with a negative position', () { + const String text = 'abc'; + final _FakeEditableTextState delegate = _FakeEditableTextState() + ..textEditingValue = const TextEditingValue(text: text); + final ViewportOffset viewportOffset = ViewportOffset.zero(); + final RenderEditable editable = RenderEditable( + backgroundCursorColor: Colors.grey, + selectionColor: Colors.black, + textDirection: TextDirection.ltr, + cursorColor: Colors.red, + offset: viewportOffset, + textSelectionDelegate: delegate, + startHandleLayerLink: LayerLink(), + endHandleLayerLink: LayerLink(), + text: const TextSpan( + text: text, + style: TextStyle(height: 1.0, fontSize: 10.0), + ), + ); + + layout(editable, onErrors: expectNoFlutterErrors); + + // Cause text metrics to be computed. + editable.computeDistanceToActualBaseline(TextBaseline.alphabetic); + + final TextSelection selection; + try { + selection = editable.getWordAtOffset(const TextPosition( + offset: -1, + affinity: TextAffinity.upstream, + )); + } catch (error) { + // In debug mode, negative offsets are caught by an assertion. + expect(error, isA()); + return; + } + + // Web's Paragraph.getWordBoundary behaves differently for a negative + // position. + if (kIsWeb) { + expect(selection, const TextSelection.collapsed(offset: 0)); + } else { + expect(selection, const TextSelection.collapsed(offset: text.length)); + } + }); } class _TestRenderEditable extends RenderEditable {