Unverified Commit 48111197 authored by Greg Price's avatar Greg Price Committed by GitHub

Fix out-of-bounds and reversed TextBox queries in computing caret metrics (#122480)

Fix out-of-bounds and reversed TextBox queries in computing caret metrics
parent 017aed4c
......@@ -1044,6 +1044,7 @@ class TextPainter {
// Get the caret metrics (in logical pixels) based off the near edge of the
// character upstream from the given string offset.
_CaretMetrics? _getMetricsFromUpstream(int offset) {
assert(offset >= 0);
final int plainTextLength = plainText.length;
if (plainTextLength == 0 || offset > plainTextLength) {
return null;
......@@ -1061,7 +1062,7 @@ class TextPainter {
final int prevRuneOffset = offset - graphemeClusterLength;
// Use BoxHeightStyle.strut to ensure that the caret's height fits within
// the line's height and is consistent throughout the line.
boxes = _paragraph!.getBoxesForRange(prevRuneOffset, offset, boxHeightStyle: ui.BoxHeightStyle.strut);
boxes = _paragraph!.getBoxesForRange(max(0, prevRuneOffset), offset, boxHeightStyle: ui.BoxHeightStyle.strut);
// When the range does not include a full cluster, no boxes will be returned.
if (boxes.isEmpty) {
// When we are at the beginning of the line, a non-surrogate position will
......@@ -1079,7 +1080,12 @@ class TextPainter {
graphemeClusterLength *= 2;
continue;
}
final TextBox box = boxes.first;
// Try to identify the box nearest the offset. This logic works when
// there's just one box, and when all boxes have the same direction.
// It may not work in bidi text: https://github.com/flutter/flutter/issues/123424
final TextBox box = boxes.last.direction == TextDirection.ltr
? boxes.last : boxes.first;
return prevCodeUnit == NEWLINE_CODE_UNIT
? _EmptyLineCaretMetrics(lineVerticalOffset: box.bottom)
......@@ -1087,11 +1093,13 @@ class TextPainter {
}
return null;
}
// Get the caret metrics (in logical pixels) based off the near edge of the
// character downstream from the given string offset.
_CaretMetrics? _getMetricsFromDownstream(int offset) {
assert(offset >= 0);
final int plainTextLength = plainText.length;
if (plainTextLength == 0 || offset < 0) {
if (plainTextLength == 0) {
return null;
}
// We cap the offset at the final index of plain text.
......@@ -1123,7 +1131,13 @@ class TextPainter {
graphemeClusterLength *= 2;
continue;
}
final TextBox box = boxes.last;
// Try to identify the box nearest the offset. This logic works when
// there's just one box, and when all boxes have the same direction.
// It may not work in bidi text: https://github.com/flutter/flutter/issues/123424
final TextBox box = boxes.first.direction == TextDirection.ltr
? boxes.first : boxes.last;
return _LineCaretMetrics(offset: Offset(box.start, box.top), writingDirection: box.direction, fullHeight: box.bottom - box.top);
}
return null;
......@@ -1159,7 +1173,13 @@ class TextPainter {
///
/// Valid only after [layout] has been called.
Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) {
final _CaretMetrics caretMetrics = _computeCaretMetrics(position);
final _CaretMetrics caretMetrics;
if (position.offset < 0) {
// TODO(LongCatIsLooong): make this case impossible; see https://github.com/flutter/flutter/issues/79495
caretMetrics = const _EmptyLineCaretMetrics(lineVerticalOffset: 0);
} else {
caretMetrics = _computeCaretMetrics(position);
}
if (caretMetrics is _EmptyLineCaretMetrics) {
final double paintOffsetAlignment = _computePaintOffsetFraction(textAlign, textDirection!);
......@@ -1192,6 +1212,10 @@ class TextPainter {
///
/// Valid only after [layout] has been called.
double? getFullHeightForCaret(TextPosition position, Rect caretPrototype) {
if (position.offset < 0) {
// TODO(LongCatIsLooong): make this case impossible; see https://github.com/flutter/flutter/issues/79495
return null;
}
final _CaretMetrics caretMetrics = _computeCaretMetrics(position);
return caretMetrics is _LineCaretMetrics ? caretMetrics.fullHeight : null;
}
......@@ -1232,10 +1256,11 @@ class TextPainter {
/// Returns a list of rects that bound the given selection.
///
/// The [selection] must be a valid range (with [TextSelection.isValid] true).
///
/// The [boxHeightStyle] and [boxWidthStyle] arguments may be used to select
/// the shape of the [TextBox]s. These properties default to
/// [ui.BoxHeightStyle.tight] and [ui.BoxWidthStyle.tight] respectively and
/// must not be null.
/// [ui.BoxHeightStyle.tight] and [ui.BoxWidthStyle.tight] respectively.
///
/// A given selection might have more than one rect if this text painter
/// contains bidirectional text because logically contiguous text might not be
......@@ -1253,6 +1278,7 @@ class TextPainter {
ui.BoxWidthStyle boxWidthStyle = ui.BoxWidthStyle.tight,
}) {
assert(_debugAssertTextLayoutIsValid);
assert(selection.isValid);
return _paragraph!.getBoxesForRange(
selection.start,
selection.end,
......
......@@ -720,6 +720,11 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
void _updateSelectionExtentsVisibility(Offset effectiveOffset) {
assert(selection != null);
if (!selection!.isValid) {
_selectionStartInViewport.value = false;
_selectionEndInViewport.value = false;
return;
}
final Rect visibleRegion = Offset.zero & size;
final Offset startOffset = _textPainter.getOffsetForCaret(
......@@ -3010,8 +3015,7 @@ class _FloatingCursorPainter extends RenderEditablePainter {
final TextSelection? selection = renderEditable.selection;
// TODO(LongCatIsLooong): skip painting the caret when the selection is
// (-1, -1).
// TODO(LongCatIsLooong): skip painting caret when selection is (-1, -1): https://github.com/flutter/flutter/issues/79495
if (selection == null || !selection.isCollapsed) {
return;
}
......
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