Use full textspan tree instead of top level textspan (#25574)
diff --git a/packages/flutter/lib/src/painting/text_painter.dart b/packages/flutter/lib/src/painting/text_painter.dart index 3af0629..906fc92 100644 --- a/packages/flutter/lib/src/painting/text_painter.dart +++ b/packages/flutter/lib/src/painting/text_painter.dart
@@ -420,6 +420,7 @@ // TODO(garyq): Use actual extended grapheme cluster length instead of // an increasing cluster length amount to achieve deterministic performance. Offset _getOffsetFromUpstream(int offset, Rect caretPrototype) { + final String flattenedText = _text.toPlainText(); final int prevCodeUnit = _text.codeUnitAt(max(0, offset - 1)); if (prevCodeUnit == null) return null; @@ -427,7 +428,7 @@ final bool needsSearch = _isUtf16Surrogate(prevCodeUnit) || _text.codeUnitAt(offset) == _zwjUtf16; int graphemeClusterLength = needsSearch ? 2 : 1; List<TextBox> boxes = <TextBox>[]; - while (boxes.isEmpty && _text.text != null) { + while (boxes.isEmpty && flattenedText != null) { final int prevRuneOffset = offset - graphemeClusterLength; boxes = _paragraph.getBoxesForRange(prevRuneOffset, offset); // When the range does not include a full cluster, no boxes will be returned. @@ -436,7 +437,7 @@ // return empty boxes. We break and try from downstream instead. if (!needsSearch) break; // Only perform one iteration if no search is required. - if (prevRuneOffset < -_text.text.length) + if (prevRuneOffset < -flattenedText.length) break; // Stop iterating when beyond the max length of the text. // Multiply by two to log(n) time cover the entire text span. This allows // faster discovery of very long clusters and reduces the possibility @@ -445,7 +446,7 @@ graphemeClusterLength *= 2; continue; } - final TextBox box = boxes[0]; + final TextBox box = boxes.first; final double caretEnd = box.end; final double dx = box.direction == TextDirection.rtl ? caretEnd - caretPrototype.width : caretEnd; return Offset(dx, box.top); @@ -456,15 +457,16 @@ // TODO(garyq): Use actual extended grapheme cluster length instead of // an increasing cluster length amount to achieve deterministic performance. Offset _getOffsetFromDownstream(int offset, Rect caretPrototype) { + final String flattenedText = _text.toPlainText(); // We cap the offset at the final index of the _text. - final int nextCodeUnit = _text.codeUnitAt(min(offset, _text.text == null ? 0 : _text.text.length - 1)); + final int nextCodeUnit = _text.codeUnitAt(min(offset, flattenedText == null ? 0 : flattenedText.length - 1)); if (nextCodeUnit == null) return null; // Check for multi-code-unit glyphs such as emojis or zero width joiner final bool needsSearch = _isUtf16Surrogate(nextCodeUnit) || nextCodeUnit == _zwjUtf16; int graphemeClusterLength = needsSearch ? 2 : 1; List<TextBox> boxes = <TextBox>[]; - while (boxes.isEmpty && _text.text != null) { + while (boxes.isEmpty && flattenedText != null) { final int nextRuneOffset = offset + graphemeClusterLength; boxes = _paragraph.getBoxesForRange(offset, nextRuneOffset); // When the range does not include a full cluster, no boxes will be returned. @@ -473,7 +475,7 @@ // return empty boxes. We break and try from upstream instead. if (!needsSearch) break; // Only perform one iteration if no search is required. - if (nextRuneOffset >= _text.text.length << 1) + if (nextRuneOffset >= flattenedText.length << 1) break; // Stop iterating when beyond the max length of the text. // Multiply by two to log(n) time cover the entire text span. This allows // faster discovery of very long clusters and reduces the possibility @@ -482,7 +484,7 @@ graphemeClusterLength *= 2; continue; } - final TextBox box = boxes[0]; + final TextBox box = boxes.last; final double caretStart = box.start; final double dx = box.direction == TextDirection.rtl ? caretStart - caretPrototype.width : caretStart; return Offset(dx, box.top);
diff --git a/packages/flutter/test/painting/text_painter_test.dart b/packages/flutter/test/painting/text_painter_test.dart index 06c5ded..d2105e8 100644 --- a/packages/flutter/test/painting/text_painter_test.dart +++ b/packages/flutter/test/painting/text_painter_test.dart
@@ -29,6 +29,31 @@ expect(caretOffset.dx, painter.width); }); + test('TextPainter null text test', () { + final TextPainter painter = TextPainter() + ..textDirection = TextDirection.ltr; + + List<TextSpan> children = <TextSpan>[const TextSpan(text: 'B'), const TextSpan(text: 'C')]; + painter.text = TextSpan(text: null, children: children); + painter.layout(); + + Offset caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 0), ui.Rect.zero); + expect(caretOffset.dx, 0); + caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 1), ui.Rect.zero); + expect(caretOffset.dx, painter.width / 2); + caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 2), ui.Rect.zero); + expect(caretOffset.dx, painter.width); + + children = <TextSpan>[]; + painter.text = TextSpan(text: null, children: children); + painter.layout(); + + caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 0), ui.Rect.zero); + expect(caretOffset.dx, 0); + caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 1), ui.Rect.zero); + expect(caretOffset.dx, 0); + }); + test('TextPainter caret emoji test', () { final TextPainter painter = TextPainter() ..textDirection = TextDirection.ltr;