Skip to content

Cache TextPainter plain text value to improve performance #109841

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 25 commits into from
Oct 25, 2022
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
09228a9
Cache plain text in TextPainter
tgucio Aug 19, 2022
8ebf720
Use cached TextPainter plain text in Editable and above
tgucio Aug 19, 2022
a97a8a5
Missed change in _getRectFromUpstream
tgucio Aug 19, 2022
42732d3
Update comment
tgucio Aug 19, 2022
17d1f58
Trailing space
tgucio Aug 19, 2022
a091e8d
Handle out of bounds offsets in _getRectFrom*
tgucio Aug 19, 2022
2740136
Merge branch 'flutter:master' into editable-plaintext-cache
tgucio Aug 31, 2022
dbb3a38
Merge remote-tracking branch 'upstream/master' into editable-plaintex…
tgucio Sep 19, 2022
363caf5
Merge branch 'flutter:master' into editable-plaintext-cache
tgucio Sep 19, 2022
ab8d597
Update comment
tgucio Sep 19, 2022
3dc0832
Update comments in editable.dart
tgucio Sep 19, 2022
744a162
Add a test in TextPainter
tgucio Sep 19, 2022
071df5b
Add a test in RenderEditable
tgucio Sep 19, 2022
f3e74f3
Fix after merge
tgucio Sep 19, 2022
bc87f6e
Fix analyzer warnings
tgucio Sep 19, 2022
0062e7e
More analyzer warnings
tgucio Sep 20, 2022
bea8460
Handle null text
tgucio Sep 20, 2022
cd72e52
Pass placeholder dimensions for test
tgucio Sep 20, 2022
f8e0071
Remove extra newline
tgucio Oct 5, 2022
57fbadf
Merge branch 'master' into editable-plaintext-cache
tgucio Oct 5, 2022
f876832
Fix merge error
tgucio Oct 5, 2022
4a8f699
Merge branch 'flutter:master' into editable-plaintext-cache
tgucio Oct 6, 2022
782e462
Remove trailing spaces
tgucio Oct 6, 2022
e70e455
Merge branch 'flutter:master' into editable-plaintext-cache
tgucio Oct 14, 2022
a6e7526
Merge branch 'master' into editable-plaintext-cache
tgucio Oct 25, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 21 additions & 13 deletions packages/flutter/lib/src/painting/text_painter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -259,10 +259,7 @@ class TextPainter {
/// This and [textDirection] must be non-null before you call [layout].
///
/// The [InlineSpan] this provides is in the form of a tree that may contain
/// multiple instances of [TextSpan]s and [WidgetSpan]s. To obtain a plain text
/// representation of the contents of this [TextPainter], use [InlineSpan.toPlainText]
/// to get the full contents of all nodes in the tree. [TextSpan.text] will
/// only provide the contents of the first node in the tree.
/// multiple instances of [TextSpan]s and [WidgetSpan]s.
InlineSpan? get text => _text;
InlineSpan? _text;
set text(InlineSpan? value) {
Expand All @@ -279,6 +276,7 @@ class TextPainter {
: _text?.compareTo(value) ?? RenderComparison.layout;

_text = value;
_cachedPlainText = null;

if (comparison.index >= RenderComparison.layout.index) {
markNeedsLayout();
Expand All @@ -290,6 +288,15 @@ class TextPainter {
// Neither relayout or repaint is needed.
}

/// Returns a plain text version of the text to paint.
///
/// This uses [InlineSpan.toPlainText] to get the full contents of all nodes in the tree.
String get plainText {
_cachedPlainText ??= _text!.toPlainText(includeSemanticsLabels: false);
return _cachedPlainText!;
}
String? _cachedPlainText;

/// How the text should be aligned horizontally.
///
/// After this is set, you must call [layout] before the next call to [paint].
Expand Down Expand Up @@ -802,11 +809,11 @@ class TextPainter {
// Get the Rect of the cursor (in logical pixels) based off the near edge
// of the character upstream from the given string offset.
Rect? _getRectFromUpstream(int offset, Rect caretPrototype) {
final String flattenedText = _text!.toPlainText(includeSemanticsLabels: false);
final int? prevCodeUnit = _text!.codeUnitAt(max(0, offset - 1));
if (prevCodeUnit == null) {
final int plainTextLength = plainText.length;
if (plainTextLength == 0 || offset > plainTextLength) {
return null;
}
final int prevCodeUnit = plainText.codeUnitAt(max(0, offset - 1));

// If the upstream character is a newline, cursor is at start of next line
const int NEWLINE_CODE_UNIT = 10;
Expand All @@ -827,7 +834,7 @@ class TextPainter {
if (!needsSearch && prevCodeUnit == NEWLINE_CODE_UNIT) {
break; // Only perform one iteration if no search is required.
}
if (prevRuneOffset < -flattenedText.length) {
if (prevRuneOffset < -plainTextLength) {
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
Expand All @@ -854,12 +861,13 @@ class TextPainter {
// Get the Rect of the cursor (in logical pixels) based off the near edge
// of the character downstream from the given string offset.
Rect? _getRectFromDownstream(int offset, Rect caretPrototype) {
final String flattenedText = _text!.toPlainText(includeSemanticsLabels: false);
// We cap the offset at the final index of the _text.
final int? nextCodeUnit = _text!.codeUnitAt(min(offset, flattenedText.length - 1));
if (nextCodeUnit == null) {
final int plainTextLength = plainText.length;
if (plainTextLength == 0 || offset < 0) {
return null;
}
// We cap the offset at the final index of plain text.
final int nextCodeUnit = plainText.codeUnitAt(min(offset, plainTextLength - 1));

// Check for multi-code-unit glyphs such as emojis or zero width joiner
final bool needsSearch = _isUtf16Surrogate(nextCodeUnit) || nextCodeUnit == _zwjUtf16 || _isUnicodeDirectionality(nextCodeUnit);
int graphemeClusterLength = needsSearch ? 2 : 1;
Expand All @@ -876,7 +884,7 @@ class TextPainter {
if (!needsSearch) {
break; // Only perform one iteration if no search is required.
}
if (nextRuneOffset >= flattenedText.length << 1) {
if (nextRuneOffset >= plainTextLength << 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
Expand Down
26 changes: 9 additions & 17 deletions packages/flutter/lib/src/rendering/editable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -658,7 +658,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
final TextRange line = _textPainter.getLineBoundary(position);
// If text is obscured, the entire string should be treated as one line.
if (obscureText) {
return TextSelection(baseOffset: 0, extentOffset: _plainText.length);
return TextSelection(baseOffset: 0, extentOffset: plainText.length);
}
return TextSelection(baseOffset: line.start, extentOffset: line.end);
}
Expand Down Expand Up @@ -727,12 +727,12 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,

void _setSelection(TextSelection nextSelection, SelectionChangedCause cause) {
if (nextSelection.isValid) {
// The nextSelection is calculated based on _plainText, which can be out
// The nextSelection is calculated based on plainText, which can be out
// of sync with the textSelectionDelegate.textEditingValue by one frame.
// This is due to the render editable and editable text handle pointer
// event separately. If the editable text changes the text during the
// event handler, the render editable will use the outdated text stored in
// the _plainText when handling the pointer event.
// the plainText when handling the pointer event.
//
// If this happens, we need to make sure the new selection is still valid.
final int textLength = textSelectionDelegate.textEditingValue.text.length;
Expand Down Expand Up @@ -774,15 +774,8 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
_textLayoutLastMinWidth = null;
}

String? _cachedPlainText;
// Returns a plain text version of the text in the painter.
//
// Returns the obscured text when [obscureText] is true. See
// [obscureText] and [obscuringCharacter].
String get _plainText {
_cachedPlainText ??= _textPainter.text!.toPlainText(includeSemanticsLabels: false);
return _cachedPlainText!;
}
/// Returns a plain text version of the text in the painter.
String get plainText => _textPainter.plainText;

/// The text to display.
InlineSpan? get text => _textPainter.text;
Expand All @@ -794,7 +787,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
return;
}
_textPainter.text = value;
_cachedPlainText = null;
_cachedAttributedValue = null;
_cachedCombinedSemanticsInfos = null;
_extractPlaceholderSpans(value);
Expand Down Expand Up @@ -1293,7 +1285,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
}
if (_cachedAttributedValue == null) {
if (obscureText) {
_cachedAttributedValue = AttributedString(obscuringCharacter * _plainText.length);
_cachedAttributedValue = AttributedString(obscuringCharacter * plainText.length);
} else {
final StringBuffer buffer = StringBuffer();
int offset = 0;
Expand Down Expand Up @@ -1813,7 +1805,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,

// Set the height based on the content.
if (width == double.infinity) {
final String text = _plainText;
final String text = plainText;
int lines = 1;
for (int index = 0; index < text.length; index += 1) {
// Count explicit line breaks.
Expand Down Expand Up @@ -2077,15 +2069,15 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
}
// If text is obscured, the entire sentence should be treated as one word.
if (obscureText) {
return TextSelection(baseOffset: 0, extentOffset: _plainText.length);
return TextSelection(baseOffset: 0, extentOffset: plainText.length);
// 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.
//
// 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))
} else if (TextLayoutMetrics.isWhitespace(plainText.codeUnitAt(position.offset))
&& position.offset > 0) {
assert(defaultTargetPlatform != null);
final TextRange? previousWord = _getPreviousWord(word.start);
Expand Down
2 changes: 1 addition & 1 deletion packages/flutter/lib/src/widgets/editable_text.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3128,7 +3128,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
return;
}

final String text = renderEditable.text?.toPlainText(includeSemanticsLabels: false) ?? '';
final String text = renderEditable.plainText;
final List<Rect> firstSelectionBoxes = renderEditable.getBoxesForSelection(const TextSelection(baseOffset: 0, extentOffset: 1));
final Rect? firstRect = firstSelectionBoxes.isNotEmpty ? firstSelectionBoxes.first : null;
final ScrollDirection scrollDirection = _scrollController.position.userScrollDirection;
Expand Down
8 changes: 2 additions & 6 deletions packages/flutter/lib/src/widgets/text_selection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -474,8 +474,6 @@ class TextSelectionOverlay {
}

double _getStartGlyphHeight() {
final InlineSpan span = renderObject.text!;
final String prevText = span.toPlainText();
final String currText = selectionDelegate.textEditingValue.text;
final int firstSelectedGraphemeExtent;
Rect? startHandleRect;
Expand All @@ -486,7 +484,7 @@ class TextSelectionOverlay {
// widget.renderObject.getRectForComposingRange might fail. In cases where
// the current frame is different from the previous we fall back to
// renderObject.preferredLineHeight.
if (prevText == currText && _selection != null && _selection.isValid && !_selection.isCollapsed) {
if (renderObject.plainText == currText && _selection != null && _selection.isValid && !_selection.isCollapsed) {
final String selectedGraphemes = _selection.textInside(currText);
firstSelectedGraphemeExtent = selectedGraphemes.characters.first.length;
startHandleRect = renderObject.getRectForComposingRange(TextRange(start: _selection.start, end: _selection.start + firstSelectedGraphemeExtent));
Expand All @@ -495,13 +493,11 @@ class TextSelectionOverlay {
}

double _getEndGlyphHeight() {
final InlineSpan span = renderObject.text!;
final String prevText = span.toPlainText();
final String currText = selectionDelegate.textEditingValue.text;
final int lastSelectedGraphemeExtent;
Rect? endHandleRect;
// See the explanation in _getStartGlyphHeight.
if (prevText == currText && _selection != null && _selection.isValid && !_selection.isCollapsed) {
if (renderObject.plainText == currText && _selection != null && _selection.isValid && !_selection.isCollapsed) {
final String selectedGraphemes = _selection.textInside(currText);
lastSelectedGraphemeExtent = selectedGraphemes.characters.last.length;
endHandleRect = renderObject.getRectForComposingRange(TextRange(start: _selection.end - lastSelectedGraphemeExtent, end: _selection.end));
Expand Down