Unverified Commit 76be5581 authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

Move caret/highlight painting to custom painters (#72828)

parent 09adc359
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:math' as math; import 'dart:math' as math;
import 'dart:ui' as ui show TextBox, lerpDouble, BoxHeightStyle, BoxWidthStyle; import 'dart:ui' as ui show TextBox, BoxHeightStyle, BoxWidthStyle;
import 'package:characters/characters.dart'; import 'package:characters/characters.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
...@@ -12,6 +12,7 @@ import 'package:flutter/semantics.dart'; ...@@ -12,6 +12,7 @@ import 'package:flutter/semantics.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'box.dart'; import 'box.dart';
import 'custom_paint.dart';
import 'layer.dart'; import 'layer.dart';
import 'object.dart'; import 'object.dart';
import 'viewport_offset.dart'; import 'viewport_offset.dart';
...@@ -21,10 +22,10 @@ const double _kCaretHeightOffset = 2.0; // pixels ...@@ -21,10 +22,10 @@ const double _kCaretHeightOffset = 2.0; // pixels
// The additional size on the x and y axis with which to expand the prototype // The additional size on the x and y axis with which to expand the prototype
// cursor to render the floating cursor in pixels. // cursor to render the floating cursor in pixels.
const Offset _kFloatingCaretSizeIncrease = Offset(0.5, 1.0); const EdgeInsets _kFloatingCaretSizeIncrease = EdgeInsets.symmetric(horizontal: 0.5, vertical: 1.0);
// The corner radius of the floating cursor in pixels. // The corner radius of the floating cursor in pixels.
const double _kFloatingCaretRadius = 1.0; const Radius _kFloatingCaretRadius = Radius.circular(1.0);
/// Signature for the callback that reports when the user changes the selection /// Signature for the callback that reports when the user changes the selection
/// (including the cursor location). /// (including the cursor location).
...@@ -211,16 +212,18 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -211,16 +212,18 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
double? cursorHeight, double? cursorHeight,
Radius? cursorRadius, Radius? cursorRadius,
bool paintCursorAboveText = false, bool paintCursorAboveText = false,
Offset? cursorOffset, Offset cursorOffset = Offset.zero,
double devicePixelRatio = 1.0, double devicePixelRatio = 1.0,
ui.BoxHeightStyle selectionHeightStyle = ui.BoxHeightStyle.tight, ui.BoxHeightStyle selectionHeightStyle = ui.BoxHeightStyle.tight,
ui.BoxWidthStyle selectionWidthStyle = ui.BoxWidthStyle.tight, ui.BoxWidthStyle selectionWidthStyle = ui.BoxWidthStyle.tight,
bool? enableInteractiveSelection, bool? enableInteractiveSelection,
EdgeInsets floatingCursorAddedMargin = const EdgeInsets.fromLTRB(4, 4, 4, 5), this.floatingCursorAddedMargin = const EdgeInsets.fromLTRB(4, 4, 4, 5),
TextRange? promptRectRange, TextRange? promptRectRange,
Color? promptRectColor, Color? promptRectColor,
Clip clipBehavior = Clip.hardEdge, Clip clipBehavior = Clip.hardEdge,
required this.textSelectionDelegate, required this.textSelectionDelegate,
RenderEditablePainter? painter,
RenderEditablePainter? foregroundPainter,
}) : assert(textAlign != null), }) : assert(textAlign != null),
assert(textDirection != null, 'RenderEditable created without a textDirection.'), assert(textDirection != null, 'RenderEditable created without a textDirection.'),
assert(maxLines == null || maxLines > 0), assert(maxLines == null || maxLines > 0),
...@@ -262,40 +265,139 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -262,40 +265,139 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
textHeightBehavior: textHeightBehavior, textHeightBehavior: textHeightBehavior,
textWidthBasis: textWidthBasis, textWidthBasis: textWidthBasis,
), ),
_cursorColor = cursorColor,
_backgroundCursorColor = backgroundCursorColor,
_showCursor = showCursor ?? ValueNotifier<bool>(false), _showCursor = showCursor ?? ValueNotifier<bool>(false),
_maxLines = maxLines, _maxLines = maxLines,
_minLines = minLines, _minLines = minLines,
_expands = expands, _expands = expands,
_selectionColor = selectionColor,
_selection = selection, _selection = selection,
_offset = offset, _offset = offset,
_cursorWidth = cursorWidth, _cursorWidth = cursorWidth,
_cursorHeight = cursorHeight, _cursorHeight = cursorHeight,
_cursorRadius = cursorRadius,
_paintCursorOnTop = paintCursorAboveText, _paintCursorOnTop = paintCursorAboveText,
_cursorOffset = cursorOffset,
_floatingCursorAddedMargin = floatingCursorAddedMargin,
_enableInteractiveSelection = enableInteractiveSelection, _enableInteractiveSelection = enableInteractiveSelection,
_devicePixelRatio = devicePixelRatio, _devicePixelRatio = devicePixelRatio,
_selectionHeightStyle = selectionHeightStyle,
_selectionWidthStyle = selectionWidthStyle,
_startHandleLayerLink = startHandleLayerLink, _startHandleLayerLink = startHandleLayerLink,
_endHandleLayerLink = endHandleLayerLink, _endHandleLayerLink = endHandleLayerLink,
_obscuringCharacter = obscuringCharacter, _obscuringCharacter = obscuringCharacter,
_obscureText = obscureText, _obscureText = obscureText,
_readOnly = readOnly, _readOnly = readOnly,
_forceLine = forceLine, _forceLine = forceLine,
_promptRectRange = promptRectRange,
_clipBehavior = clipBehavior { _clipBehavior = clipBehavior {
assert(_showCursor != null); assert(_showCursor != null);
assert(!_showCursor.value || cursorColor != null); assert(!_showCursor.value || cursorColor != null);
this.hasFocus = hasFocus ?? false; this.hasFocus = hasFocus ?? false;
if (promptRectColor != null)
_promptRectPaint.color = promptRectColor; _selectionPainter.highlightColor = selectionColor;
_selectionPainter.highlightedRange = selection;
_selectionPainter.selectionHeightStyle = selectionHeightStyle;
_selectionPainter.selectionWidthStyle = selectionWidthStyle;
_autocorrectHighlightPainter.highlightColor = promptRectColor;
_autocorrectHighlightPainter.highlightedRange = promptRectRange;
_caretPainter.caretColor = cursorColor;
_caretPainter.cursorRadius = cursorRadius;
_caretPainter.cursorOffset = cursorOffset;
_caretPainter.backgroundCursorColor = backgroundCursorColor;
_updateForegroundPainter(foregroundPainter);
_updatePainter(painter);
}
/// Child render objects
_RenderEditableCustomPaint? _foregroundRenderObject;
_RenderEditableCustomPaint? _backgroundRenderObject;
void _updateForegroundPainter(RenderEditablePainter? newPainter) {
final _CompositeRenderEditablePainter effectivePainter = newPainter == null
? _builtInForegroundPainters
: _CompositeRenderEditablePainter(painters: <RenderEditablePainter>[
_builtInForegroundPainters,
newPainter,
]);
if (_foregroundRenderObject == null) {
final _RenderEditableCustomPaint foregroundRenderObject = _RenderEditableCustomPaint(painter: effectivePainter);
adoptChild(foregroundRenderObject);
_foregroundRenderObject = foregroundRenderObject;
} else {
_foregroundRenderObject?.painter = effectivePainter;
}
_foregroundPainter = newPainter;
}
/// The [RenderEditablePainter] to use for painting above this
/// [RenderEditable]'s text content.
///
/// The new [RenderEditablePainter] will replace the previously specified
/// foreground painter, and schedule a repaint if the new painter's
/// `shouldRepaint` method returns true.
RenderEditablePainter? get foregroundPainter => _foregroundPainter;
RenderEditablePainter? _foregroundPainter;
set foregroundPainter(RenderEditablePainter? newPainter) {
if (newPainter == _foregroundPainter)
return;
_updateForegroundPainter(newPainter);
}
void _updatePainter(RenderEditablePainter? newPainter) {
final _CompositeRenderEditablePainter effectivePainter = newPainter == null
? _builtInPainters
: _CompositeRenderEditablePainter(painters: <RenderEditablePainter>[_builtInPainters, newPainter]);
if (_backgroundRenderObject == null) {
final _RenderEditableCustomPaint backgroundRenderObject = _RenderEditableCustomPaint(painter: effectivePainter);
adoptChild(backgroundRenderObject);
_backgroundRenderObject = backgroundRenderObject;
} else {
_backgroundRenderObject?.painter = effectivePainter;
}
_painter = newPainter;
}
/// Sets the [RenderEditablePainter] to use for painting beneath this
/// [RenderEditable]'s text content.
///
/// The new [RenderEditablePainter] will replace the previously specified
/// painter, and schedule a repaint if the new painter's `shouldRepaint`
/// method returns true.
RenderEditablePainter? get painter => _painter;
RenderEditablePainter? _painter;
set painter(RenderEditablePainter? newPainter) {
if (newPainter == _painter)
return;
_updatePainter(newPainter);
}
// Caret Painters:
// The floating painter. This painter paints the regular caret as well.
late final _FloatingCursorPainter _caretPainter = _FloatingCursorPainter(_onCaretChanged);
// Text Highlight painters:
final _TextHighlightPainter _selectionPainter = _TextHighlightPainter();
final _TextHighlightPainter _autocorrectHighlightPainter = _TextHighlightPainter();
_CompositeRenderEditablePainter get _builtInForegroundPainters => _cachedBuiltInForegroundPainters ??= _createBuiltInForegroundPainters();
_CompositeRenderEditablePainter? _cachedBuiltInForegroundPainters;
_CompositeRenderEditablePainter _createBuiltInForegroundPainters() {
return _CompositeRenderEditablePainter(
painters: <RenderEditablePainter>[
if (paintCursorAboveText) _caretPainter,
],
);
} }
_CompositeRenderEditablePainter get _builtInPainters => _cachedBuiltInPainters ??= _createBuiltInPainters();
_CompositeRenderEditablePainter? _cachedBuiltInPainters;
_CompositeRenderEditablePainter _createBuiltInPainters() {
return _CompositeRenderEditablePainter(
painters: <RenderEditablePainter>[
_autocorrectHighlightPainter,
_selectionPainter,
if (!paintCursorAboveText) _caretPainter,
],
);
}
/// Called when the selection changes. /// Called when the selection changes.
/// ///
/// If this is null, then selection changes will be ignored. /// If this is null, then selection changes will be ignored.
...@@ -304,8 +406,17 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -304,8 +406,17 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
double? _textLayoutLastMaxWidth; double? _textLayoutLastMaxWidth;
double? _textLayoutLastMinWidth; double? _textLayoutLastMinWidth;
Rect? _lastCaretRect;
// TODO(LongCatIsLooong): currently EditableText uses this callback to keep
// the text field visible. But we don't always paint the caret, for example
// when the selection is not collapsed.
/// Called during the paint phase when the caret location changes. /// Called during the paint phase when the caret location changes.
CaretChangedHandler? onCaretChanged; CaretChangedHandler? onCaretChanged;
void _onCaretChanged(Rect caretRect) {
if (_lastCaretRect != caretRect)
onCaretChanged?.call(caretRect);
_lastCaretRect = onCaretChanged == null ? null : caretRect;
}
/// Whether the [handleEvent] will propagate pointer events to selection /// Whether the [handleEvent] will propagate pointer events to selection
/// handlers. /// handlers.
...@@ -375,6 +486,23 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -375,6 +486,23 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
markNeedsSemanticsUpdate(); markNeedsSemanticsUpdate();
} }
/// Controls how tall the selection highlight boxes are computed to be.
///
/// See [ui.BoxHeightStyle] for details on available styles.
ui.BoxHeightStyle get selectionHeightStyle => _selectionPainter.selectionHeightStyle;
set selectionHeightStyle(ui.BoxHeightStyle value) {
_selectionPainter.selectionHeightStyle = value;
}
/// Controls how wide the selection highlight boxes are computed to be.
///
/// See [ui.BoxWidthStyle] for details on available styles.
ui.BoxWidthStyle get selectionWidthStyle => _selectionPainter.selectionWidthStyle;
set selectionWidthStyle(ui.BoxWidthStyle value) {
_selectionPainter.selectionWidthStyle = value;
}
/// The object that controls the text selection, used by this render object /// The object that controls the text selection, used by this render object
/// for implementing cut, copy, and paste keyboard shortcuts. /// for implementing cut, copy, and paste keyboard shortcuts.
/// ///
...@@ -382,9 +510,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -382,9 +510,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
/// with the most recently set [TextSelectionDelegate]. /// with the most recently set [TextSelectionDelegate].
TextSelectionDelegate textSelectionDelegate; TextSelectionDelegate textSelectionDelegate;
Rect? _lastCaretRect;
late Rect _currentCaretRect;
/// Track whether position of the start of the selected text is within the viewport. /// Track whether position of the start of the selected text is within the viewport.
/// ///
/// For example, if the text contains "Hello World", and the user selects /// For example, if the text contains "Hello World", and the user selects
...@@ -881,6 +1006,14 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -881,6 +1006,14 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
); );
} }
@override
void markNeedsPaint() {
super.markNeedsPaint();
// Tell the painers to repaint since text layout may have changed.
_foregroundRenderObject?.markNeedsPaint();
_backgroundRenderObject?.markNeedsPaint();
}
/// Marks the render object as needing to be laid out again and have its text /// Marks the render object as needing to be laid out again and have its text
/// metrics recomputed. /// metrics recomputed.
/// ///
...@@ -989,26 +1122,18 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -989,26 +1122,18 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
} }
/// The color to use when painting the cursor. /// The color to use when painting the cursor.
Color? get cursorColor => _cursorColor; Color? get cursorColor => _caretPainter.caretColor;
Color? _cursorColor;
set cursorColor(Color? value) { set cursorColor(Color? value) {
if (_cursorColor == value) _caretPainter.caretColor = value;
return;
_cursorColor = value;
markNeedsPaint();
} }
/// The color to use when painting the cursor aligned to the text while /// The color to use when painting the cursor aligned to the text while
/// rendering the floating cursor. /// rendering the floating cursor.
/// ///
/// The default is light grey. /// The default is light grey.
Color? get backgroundCursorColor => _backgroundCursorColor; Color? get backgroundCursorColor => _caretPainter.backgroundCursorColor;
Color? _backgroundCursorColor;
set backgroundCursorColor(Color? value) { set backgroundCursorColor(Color? value) {
if (backgroundCursorColor == value) _caretPainter.backgroundCursorColor = value;
return;
_backgroundCursorColor = value;
markNeedsPaint();
} }
/// Whether to paint the cursor. /// Whether to paint the cursor.
...@@ -1019,11 +1144,16 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -1019,11 +1144,16 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
if (_showCursor == value) if (_showCursor == value)
return; return;
if (attached) if (attached)
_showCursor.removeListener(markNeedsPaint); _showCursor.removeListener(_showHideCursor);
_showCursor = value; _showCursor = value;
if (attached) if (attached) {
_showCursor.addListener(markNeedsPaint); _showHideCursor();
markNeedsPaint(); _showCursor.addListener(_showHideCursor);
}
}
void _showHideCursor() {
_caretPainter.shouldPaint = showCursor.value;
} }
/// Whether the editable is currently focused. /// Whether the editable is currently focused.
...@@ -1114,13 +1244,9 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -1114,13 +1244,9 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
} }
/// The color to use when painting the selection. /// The color to use when painting the selection.
Color? get selectionColor => _selectionColor; Color? get selectionColor => _selectionPainter.highlightColor;
Color? _selectionColor;
set selectionColor(Color? value) { set selectionColor(Color? value) {
if (_selectionColor == value) _selectionPainter.highlightColor = value;
return;
_selectionColor = value;
markNeedsPaint();
} }
/// The number of font pixels for each logical pixel. /// The number of font pixels for each logical pixel.
...@@ -1136,8 +1262,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -1136,8 +1262,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
markNeedsTextLayout(); markNeedsTextLayout();
} }
List<ui.TextBox>? _selectionRects;
/// The region of text that is selected, if any. /// The region of text that is selected, if any.
/// ///
/// The caret position is represented by a collapsed selection. /// The caret position is represented by a collapsed selection.
...@@ -1150,7 +1274,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -1150,7 +1274,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
if (_selection == value) if (_selection == value)
return; return;
_selection = value; _selection = value;
_selectionRects = null; _selectionPainter.highlightedRange = value;
markNeedsPaint(); markNeedsPaint();
markNeedsSemanticsUpdate(); markNeedsSemanticsUpdate();
} }
...@@ -1212,7 +1336,12 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -1212,7 +1336,12 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
if (_paintCursorOnTop == value) if (_paintCursorOnTop == value)
return; return;
_paintCursorOnTop = value; _paintCursorOnTop = value;
markNeedsLayout(); // Clear cached built-in painters and reconfigure painters.
_cachedBuiltInForegroundPainters = null;
_cachedBuiltInPainters = null;
// Call update methods to rebuild and set the effective painters.
_updateForegroundPainter(_foregroundPainter);
_updatePainter(_painter);
} }
/// {@template flutter.rendering.RenderEditable.cursorOffset} /// {@template flutter.rendering.RenderEditable.cursorOffset}
...@@ -1223,25 +1352,17 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -1223,25 +1352,17 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
/// platforms. The origin from where the offset is applied to is the arbitrary /// platforms. The origin from where the offset is applied to is the arbitrary
/// location where the cursor ends up being rendered from by default. /// location where the cursor ends up being rendered from by default.
/// {@endtemplate} /// {@endtemplate}
Offset? get cursorOffset => _cursorOffset; Offset get cursorOffset => _caretPainter.cursorOffset;
Offset? _cursorOffset; set cursorOffset(Offset value) {
set cursorOffset(Offset? value) { _caretPainter.cursorOffset = value;
if (_cursorOffset == value)
return;
_cursorOffset = value;
markNeedsLayout();
} }
/// How rounded the corners of the cursor should be. /// How rounded the corners of the cursor should be.
/// ///
/// A null value is the same as [Radius.zero]. /// A null value is the same as [Radius.zero].
Radius? get cursorRadius => _cursorRadius; Radius? get cursorRadius => _caretPainter.cursorRadius;
Radius? _cursorRadius;
set cursorRadius(Radius? value) { set cursorRadius(Radius? value) {
if (_cursorRadius == value) _caretPainter.cursorRadius = value;
return;
_cursorRadius = value;
markNeedsPaint();
} }
/// The [LayerLink] of start selection handle. /// The [LayerLink] of start selection handle.
...@@ -1274,45 +1395,11 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -1274,45 +1395,11 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
/// moving the floating cursor. /// moving the floating cursor.
/// ///
/// Defaults to a padding with left, top and right set to 4, bottom to 5. /// Defaults to a padding with left, top and right set to 4, bottom to 5.
EdgeInsets get floatingCursorAddedMargin => _floatingCursorAddedMargin; EdgeInsets floatingCursorAddedMargin;
EdgeInsets _floatingCursorAddedMargin;
set floatingCursorAddedMargin(EdgeInsets value) {
if (_floatingCursorAddedMargin == value)
return;
_floatingCursorAddedMargin = value;
markNeedsPaint();
}
bool _floatingCursorOn = false; bool _floatingCursorOn = false;
late Offset _floatingCursorOffset;
late TextPosition _floatingCursorTextPosition; late TextPosition _floatingCursorTextPosition;
/// Controls how tall the selection highlight boxes are computed to be.
///
/// See [ui.BoxHeightStyle] for details on available styles.
ui.BoxHeightStyle get selectionHeightStyle => _selectionHeightStyle;
ui.BoxHeightStyle _selectionHeightStyle;
set selectionHeightStyle(ui.BoxHeightStyle value) {
assert(value != null);
if (_selectionHeightStyle == value)
return;
_selectionHeightStyle = value;
markNeedsPaint();
}
/// Controls how wide the selection highlight boxes are computed to be.
///
/// See [ui.BoxWidthStyle] for details on available styles.
ui.BoxWidthStyle get selectionWidthStyle => _selectionWidthStyle;
ui.BoxWidthStyle _selectionWidthStyle;
set selectionWidthStyle(ui.BoxWidthStyle value) {
assert(value != null);
if (_selectionWidthStyle == value)
return;
_selectionWidthStyle = value;
markNeedsPaint();
}
/// Whether to allow the user to change the selection. /// Whether to allow the user to change the selection.
/// ///
/// Since [RenderEditable] does not handle selection manipulation /// Since [RenderEditable] does not handle selection manipulation
...@@ -1366,23 +1453,11 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -1366,23 +1453,11 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
// TODO(ianh): We should change the getter to return null when _promptRectRange is null // TODO(ianh): We should change the getter to return null when _promptRectRange is null
// (otherwise, if you set it to null and then get it, you get back non-null). // (otherwise, if you set it to null and then get it, you get back non-null).
// Alternatively, we could stop supporting setting this to null. // Alternatively, we could stop supporting setting this to null.
Color? get promptRectColor => _promptRectPaint.color; Color? get promptRectColor => _autocorrectHighlightPainter.highlightColor;
set promptRectColor(Color? newValue) { set promptRectColor(Color? newValue) {
// Painter.color cannot be null. _autocorrectHighlightPainter.highlightColor = newValue;
if (newValue == null) {
setPromptRectRange(null);
return;
}
if (promptRectColor == newValue)
return;
_promptRectPaint.color = newValue;
if (_promptRectRange != null)
markNeedsPaint();
} }
TextRange? _promptRectRange;
/// Dismisses the currently displayed prompt rectangle and displays a new prompt rectangle /// Dismisses the currently displayed prompt rectangle and displays a new prompt rectangle
/// over [newRange] in the given color [promptRectColor]. /// over [newRange] in the given color [promptRectColor].
/// ///
...@@ -1390,11 +1465,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -1390,11 +1465,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
/// ///
/// When set to null, the currently displayed prompt rectangle (if any) will be dismissed. /// When set to null, the currently displayed prompt rectangle (if any) will be dismissed.
void setPromptRectRange(TextRange? newRange) { void setPromptRectRange(TextRange? newRange) {
if (_promptRectRange == newRange) _autocorrectHighlightPainter.highlightedRange = newRange;
return;
_promptRectRange = newRange;
markNeedsPaint();
} }
/// The maximum amount the text is allowed to scroll. /// The maximum amount the text is allowed to scroll.
...@@ -1558,12 +1629,16 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -1558,12 +1629,16 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
@override @override
void attach(PipelineOwner owner) { void attach(PipelineOwner owner) {
super.attach(owner); super.attach(owner);
_foregroundRenderObject?.attach(owner);
_backgroundRenderObject?.attach(owner);
_tap = TapGestureRecognizer(debugOwner: this) _tap = TapGestureRecognizer(debugOwner: this)
..onTapDown = _handleTapDown ..onTapDown = _handleTapDown
..onTap = _handleTap; ..onTap = _handleTap;
_longPress = LongPressGestureRecognizer(debugOwner: this)..onLongPress = _handleLongPress; _longPress = LongPressGestureRecognizer(debugOwner: this)..onLongPress = _handleLongPress;
_offset.addListener(markNeedsPaint); _offset.addListener(markNeedsPaint);
_showCursor.addListener(markNeedsPaint); _showHideCursor();
_showCursor.addListener(_showHideCursor);
} }
@override @override
...@@ -1571,10 +1646,32 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -1571,10 +1646,32 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
_tap.dispose(); _tap.dispose();
_longPress.dispose(); _longPress.dispose();
_offset.removeListener(markNeedsPaint); _offset.removeListener(markNeedsPaint);
_showCursor.removeListener(markNeedsPaint); _showCursor.removeListener(_showHideCursor);
if (_listenerAttached) if (_listenerAttached)
RawKeyboard.instance.removeListener(_handleKeyEvent); RawKeyboard.instance.removeListener(_handleKeyEvent);
super.detach(); super.detach();
_foregroundRenderObject?.detach();
_backgroundRenderObject?.detach();
}
@override
void redepthChildren() {
final RenderObject? foregroundChild = _foregroundRenderObject;
final RenderObject? backgroundChild = _backgroundRenderObject;
if (foregroundChild != null)
redepthChild(foregroundChild);
if (backgroundChild != null)
redepthChild(backgroundChild);
}
@override
void visitChildren(RenderObjectVisitor visitor) {
final RenderObject? foregroundChild = _foregroundRenderObject;
final RenderObject? backgroundChild = _backgroundRenderObject;
if (foregroundChild != null)
visitor(foregroundChild);
if (backgroundChild != null)
visitor(backgroundChild);
} }
bool get _isMultiline => maxLines != 1; bool get _isMultiline => maxLines != 1;
...@@ -1632,7 +1729,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -1632,7 +1729,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
final Offset paintOffset = _paintOffset; final Offset paintOffset = _paintOffset;
final List<ui.TextBox> boxes = selection.isCollapsed ? final List<ui.TextBox> boxes = selection.isCollapsed ?
<ui.TextBox>[] : _textPainter.getBoxesForSelection(selection); <ui.TextBox>[] : _textPainter.getBoxesForSelection(selection);
if (boxes.isEmpty) { if (boxes.isEmpty) {
...@@ -1703,12 +1799,9 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -1703,12 +1799,9 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth); _layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
final Offset caretOffset = _textPainter.getOffsetForCaret(caretPosition, _caretPrototype); final Offset caretOffset = _textPainter.getOffsetForCaret(caretPosition, _caretPrototype);
// This rect is the same as _caretPrototype but without the vertical padding. // This rect is the same as _caretPrototype but without the vertical padding.
Rect rect = Rect.fromLTWH(0.0, 0.0, cursorWidth, cursorHeight).shift(caretOffset + _paintOffset); final Rect rect = Rect.fromLTWH(0.0, 0.0, cursorWidth, cursorHeight).shift(caretOffset + _paintOffset + cursorOffset);
// Add additional cursor offset (generally only if on iOS). // Add additional cursor offset (generally only if on iOS).
if (_cursorOffset != null) return rect.shift(_snapToPhysicalPixel(rect.topLeft));
rect = rect.shift(_cursorOffset!);
return rect.shift(_getPixelPerfectCursorOffset(rect));
} }
@override @override
...@@ -2079,6 +2172,21 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -2079,6 +2172,21 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
} }
} }
// Computes the offset to apply to the given [sourceOffset] so it perfectly
// snaps to physical pixels.
Offset _snapToPhysicalPixel(Offset sourceOffset) {
final Offset globalOffset = localToGlobal(sourceOffset);
final double pixelMultiple = 1.0 / _devicePixelRatio;
return Offset(
globalOffset.dx.isFinite
? (globalOffset.dx / pixelMultiple).round() * pixelMultiple - globalOffset.dx
: 0,
globalOffset.dy.isFinite
? (globalOffset.dy / pixelMultiple).round() * pixelMultiple - globalOffset.dy
: 0,
);
}
@override @override
Size computeDryLayout(BoxConstraints constraints) { Size computeDryLayout(BoxConstraints constraints) {
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth); _layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
...@@ -2092,7 +2200,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -2092,7 +2200,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
final BoxConstraints constraints = this.constraints; final BoxConstraints constraints = this.constraints;
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth); _layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
_computeCaretPrototype(); _computeCaretPrototype();
_selectionRects = null;
// We grab _textPainter.size here because assigning to `size` on the next // We grab _textPainter.size here because assigning to `size` on the next
// line will trigger us to validate our intrinsic sizes, which will change // line will trigger us to validate our intrinsic sizes, which will change
// _textPainter's layout because the intrinsic size calculations are // _textPainter's layout because the intrinsic size calculations are
...@@ -2106,144 +2213,15 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -2106,144 +2213,15 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
.constrainWidth(_textPainter.size.width + _caretMargin); .constrainWidth(_textPainter.size.width + _caretMargin);
size = Size(width, constraints.constrainHeight(_preferredHeight(constraints.maxWidth))); size = Size(width, constraints.constrainHeight(_preferredHeight(constraints.maxWidth)));
final Size contentSize = Size(textPainterSize.width + _caretMargin, textPainterSize.height); final Size contentSize = Size(textPainterSize.width + _caretMargin, textPainterSize.height);
_maxScrollExtent = _getMaxScrollExtent(contentSize);
offset.applyViewportDimension(_viewportExtent);
offset.applyContentDimensions(0.0, _maxScrollExtent);
}
/// Computes the offset to apply to the given [caretRect] so it perfectly
/// snaps to physical pixels.
Offset _getPixelPerfectCursorOffset(Rect caretRect) {
final Offset caretPosition = localToGlobal(caretRect.topLeft);
final double pixelMultiple = 1.0 / _devicePixelRatio;
final double pixelPerfectOffsetX = caretPosition.dx.isFinite
? (caretPosition.dx / pixelMultiple).round() * pixelMultiple - caretPosition.dx
: 0;
final double pixelPerfectOffsetY = caretPosition.dy.isFinite
? (caretPosition.dy / pixelMultiple).round() * pixelMultiple - caretPosition.dy
: 0;
return Offset(pixelPerfectOffsetX, pixelPerfectOffsetY);
}
void _paintCaretIfNeeded(Canvas canvas, Offset effectiveOffset, TextPosition textPosition) {
assert(_textLayoutLastMaxWidth == constraints.maxWidth &&
_textLayoutLastMinWidth == constraints.minWidth,
'Last width ($_textLayoutLastMinWidth, $_textLayoutLastMaxWidth) not the same as max width constraint (${constraints.minWidth}, ${constraints.maxWidth}).');
assert(_caretPrototype != null);
// If the floating cursor is enabled, the text cursor's color is [backgroundCursorColor] while
// the floating cursor's color is _cursorColor;
final Paint paint = Paint()
..color = (_floatingCursorOn ? backgroundCursorColor : _cursorColor)!;
final Offset caretOffset = _textPainter.getOffsetForCaret(textPosition, _caretPrototype) + effectiveOffset;
Rect caretRect = _caretPrototype.shift(caretOffset);
if (_cursorOffset != null)
caretRect = caretRect.shift(_cursorOffset!);
final double? caretHeight = _textPainter.getFullHeightForCaret(textPosition, _caretPrototype);
if (caretHeight != null) {
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
final double heightDiff = caretHeight - caretRect.height;
// Center the caret vertically along the text.
caretRect = Rect.fromLTWH(
caretRect.left,
caretRect.top + heightDiff / 2,
caretRect.width,
caretRect.height,
);
break;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
// Override the height to take the full height of the glyph at the TextPosition
// when not on iOS. iOS has special handling that creates a taller caret.
// TODO(garyq): See the TODO for _computeCaretPrototype().
caretRect = Rect.fromLTWH(
caretRect.left,
caretRect.top - _kCaretHeightOffset,
caretRect.width,
caretHeight,
);
break;
}
}
caretRect = caretRect.shift(_getPixelPerfectCursorOffset(caretRect)); final BoxConstraints painterConstraints = BoxConstraints.tight(contentSize);
_currentCaretRect = caretRect;
if (!_showCursor.value) _foregroundRenderObject?.layout(painterConstraints);
return; _backgroundRenderObject?.layout(painterConstraints);
if (cursorRadius == null) {
canvas.drawRect(caretRect, paint);
} else {
final RRect caretRRect = RRect.fromRectAndRadius(caretRect, cursorRadius!);
canvas.drawRRect(caretRRect, paint);
}
}
void _updateCaretRect() {
if (_currentCaretRect != _lastCaretRect) {
_lastCaretRect = _currentCaretRect;
if (onCaretChanged != null)
onCaretChanged!(_currentCaretRect);
}
}
/// Sets the screen position of the floating cursor and the text position _maxScrollExtent = _getMaxScrollExtent(contentSize);
/// closest to the cursor. offset.applyViewportDimension(_viewportExtent);
void setFloatingCursor(FloatingCursorDragState state, Offset boundedOffset, TextPosition lastTextPosition, { double? resetLerpValue }) { offset.applyContentDimensions(0.0, _maxScrollExtent);
assert(state != null);
assert(boundedOffset != null);
assert(lastTextPosition != null);
if (state == FloatingCursorDragState.Start) {
_relativeOrigin = Offset.zero;
_previousOffset = null;
_resetOriginOnBottom = false;
_resetOriginOnTop = false;
_resetOriginOnRight = false;
_resetOriginOnBottom = false;
}
_floatingCursorOn = state != FloatingCursorDragState.End;
_resetFloatingCursorAnimationValue = resetLerpValue;
if (_floatingCursorOn) {
_floatingCursorOffset = boundedOffset;
_floatingCursorTextPosition = lastTextPosition;
}
markNeedsPaint();
}
void _paintFloatingCaret(Canvas canvas, Offset effectiveOffset) {
assert(_textLayoutLastMaxWidth == constraints.maxWidth &&
_textLayoutLastMinWidth == constraints.minWidth,
'Last width ($_textLayoutLastMinWidth, $_textLayoutLastMaxWidth) not the same as max width constraint (${constraints.minWidth}, ${constraints.maxWidth}).');
assert(_floatingCursorOn);
// We always want the floating cursor to render at full opacity.
final Paint paint = Paint()..color = _cursorColor!.withOpacity(0.75);
double sizeAdjustmentX = _kFloatingCaretSizeIncrease.dx;
double sizeAdjustmentY = _kFloatingCaretSizeIncrease.dy;
if (_resetFloatingCursorAnimationValue != null) {
sizeAdjustmentX = ui.lerpDouble(sizeAdjustmentX, 0, _resetFloatingCursorAnimationValue!)!;
sizeAdjustmentY = ui.lerpDouble(sizeAdjustmentY, 0, _resetFloatingCursorAnimationValue!)!;
}
final Rect floatingCaretPrototype = Rect.fromLTRB(
_caretPrototype.left - sizeAdjustmentX,
_caretPrototype.top - sizeAdjustmentY,
_caretPrototype.right + sizeAdjustmentX,
_caretPrototype.bottom + sizeAdjustmentY,
);
final Rect caretRect = floatingCaretPrototype.shift(effectiveOffset);
const Radius floatingCursorRadius = Radius.circular(_kFloatingCaretRadius);
final RRect caretRRect = RRect.fromRectAndRadius(caretRect, floatingCursorRadius);
canvas.drawRRect(caretRRect, paint);
} }
// The relative origin in relation to the distance the user has theoretically // The relative origin in relation to the distance the user has theoretically
...@@ -2305,32 +2283,33 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -2305,32 +2283,33 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
return adjustedOffset; return adjustedOffset;
} }
void _paintSelection(Canvas canvas, Offset effectiveOffset) { /// Sets the screen position of the floating cursor and the text position
assert(_textLayoutLastMaxWidth == constraints.maxWidth && /// closest to the cursor.
_textLayoutLastMinWidth == constraints.minWidth, void setFloatingCursor(FloatingCursorDragState state, Offset boundedOffset, TextPosition lastTextPosition, { double? resetLerpValue }) {
'Last width ($_textLayoutLastMinWidth, $_textLayoutLastMaxWidth) not the same as max width constraint (${constraints.minWidth}, ${constraints.maxWidth}).'); assert(state != null);
assert(_selectionRects != null); assert(boundedOffset != null);
final Paint paint = Paint()..color = _selectionColor!; assert(lastTextPosition != null);
for (final ui.TextBox box in _selectionRects!) if (state == FloatingCursorDragState.Start) {
canvas.drawRect(box.toRect().shift(effectiveOffset), paint); _relativeOrigin = Offset.zero;
} _previousOffset = null;
_resetOriginOnBottom = false;
final Paint _promptRectPaint = Paint(); _resetOriginOnTop = false;
void _paintPromptRectIfNeeded(Canvas canvas, Offset effectiveOffset) { _resetOriginOnRight = false;
if (_promptRectRange == null || promptRectColor == null) { _resetOriginOnBottom = false;
return;
} }
_floatingCursorOn = state != FloatingCursorDragState.End;
final List<TextBox> boxes = _textPainter.getBoxesForSelection( _resetFloatingCursorAnimationValue = resetLerpValue;
TextSelection( if (_floatingCursorOn) {
baseOffset: _promptRectRange!.start, _floatingCursorTextPosition = lastTextPosition;
extentOffset: _promptRectRange!.end, final double? animationValue = _resetFloatingCursorAnimationValue;
), final EdgeInsets sizeAdjustment = animationValue != null
); ? EdgeInsets.lerp(_kFloatingCaretSizeIncrease, EdgeInsets.zero, animationValue)!
: _kFloatingCaretSizeIncrease;
for (final TextBox box in boxes) { _caretPainter.floatingCursorRect = sizeAdjustment.inflateRect(_caretPrototype).shift(boundedOffset);
canvas.drawRect(box.toRect().shift(effectiveOffset), _promptRectPaint); } else {
_caretPainter.floatingCursorRect = null;
} }
_caretPainter.showRegularCaret = _resetFloatingCursorAnimationValue == null;
} }
void _paintContents(PaintingContext context, Offset offset) { void _paintContents(PaintingContext context, Offset offset) {
...@@ -2339,46 +2318,22 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -2339,46 +2318,22 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
'Last width ($_textLayoutLastMinWidth, $_textLayoutLastMaxWidth) not the same as max width constraint (${constraints.minWidth}, ${constraints.maxWidth}).'); 'Last width ($_textLayoutLastMinWidth, $_textLayoutLastMaxWidth) not the same as max width constraint (${constraints.minWidth}, ${constraints.maxWidth}).');
final Offset effectiveOffset = offset + _paintOffset; final Offset effectiveOffset = offset + _paintOffset;
bool showSelection = false;
bool canShowCaret = false;
if (selection != null && !_floatingCursorOn) { if (selection != null && !_floatingCursorOn) {
if (selection!.isCollapsed && cursorColor != null)
canShowCaret = true;
else if (!selection!.isCollapsed && _selectionColor != null)
showSelection = true;
_updateSelectionExtentsVisibility(effectiveOffset); _updateSelectionExtentsVisibility(effectiveOffset);
} }
if (showSelection) { final RenderBox? foregroundChild = _foregroundRenderObject;
assert(selection != null); final RenderBox? backgroundChild = _backgroundRenderObject;
_selectionRects ??= _textPainter.getBoxesForSelection(selection!, boxHeightStyle: _selectionHeightStyle, boxWidthStyle: _selectionWidthStyle);
_paintSelection(context.canvas, effectiveOffset);
}
_paintPromptRectIfNeeded(context.canvas, effectiveOffset);
// On iOS, the cursor is painted over the text, on Android, it's painted
// under it.
if (paintCursorAboveText)
_textPainter.paint(context.canvas, effectiveOffset);
if (canShowCaret) { // The painters paint in the viewport's coordinate space, since the
assert(selection != null); // textPainter's coordinate space is not known to high level widgets.
_paintCaretIfNeeded(context.canvas, effectiveOffset, selection!.extent); if (backgroundChild != null)
_updateCaretRect(); context.paintChild(backgroundChild, offset);
}
if (!paintCursorAboveText) _textPainter.paint(context.canvas, effectiveOffset);
_textPainter.paint(context.canvas, effectiveOffset);
if (_floatingCursorOn) { if (foregroundChild != null)
if (_resetFloatingCursorAnimationValue == null) { context.paintChild(foregroundChild, offset);
_paintCaretIfNeeded(context.canvas, effectiveOffset, _floatingCursorTextPosition);
_updateCaretRect();
}
_paintFloatingCaret(context.canvas, _floatingCursorOffset);
}
} }
void _paintHandleLayers(PaintingContext context, List<TextSelectionPoint> endpoints) { void _paintHandleLayers(PaintingContext context, List<TextSelectionPoint> endpoints) {
...@@ -2405,6 +2360,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -2405,6 +2360,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
); );
} }
} }
@override @override
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth); _layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
...@@ -2449,3 +2405,412 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { ...@@ -2449,3 +2405,412 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
]; ];
} }
} }
class _RenderEditableCustomPaint extends RenderBox {
_RenderEditableCustomPaint({
RenderEditablePainter? painter,
}) : _painter = painter,
super();
@override
RenderEditable? get parent => super.parent as RenderEditable?;
@override
bool get isRepaintBoundary => true;
@override
bool get sizedByParent => true;
RenderEditablePainter? get painter => _painter;
RenderEditablePainter? _painter;
set painter(RenderEditablePainter? newValue) {
if (newValue == painter)
return;
final RenderEditablePainter? oldPainter = painter;
_painter = newValue;
if (newValue?.shouldRepaint(oldPainter) ?? true)
markNeedsPaint();
if (attached) {
oldPainter?.removeListener(markNeedsPaint);
newValue?.addListener(markNeedsPaint);
}
}
@override
void paint(PaintingContext context, Offset offset) {
final RenderEditable? parent = this.parent;
assert(parent != null);
final RenderEditablePainter? painter = this.painter;
if (painter != null && parent != null) {
painter.paint(context.canvas, size, parent);
}
}
@override
void attach(PipelineOwner owner) {
super.attach(owner);
_painter?.addListener(markNeedsPaint);
}
@override
void detach() {
_painter?.removeListener(markNeedsPaint);
super.detach();
}
@override
Size computeDryLayout(BoxConstraints constraints) => constraints.biggest;
}
/// An interface that paints within a [RenderEditable]'s bounds, above or
/// beneath its text content.
///
/// This painter is typically used for painting auxiliary content that depends
/// on text layout metrics (for instance, for painting carets and text highlight
/// blocks). It can paint independently from its [RenderEditable], allowing it
/// to repaint without triggering a repaint on the entire [RenderEditable] stack
/// when only auxiliary content changes (e.g. a blinking cursor) are present. It
/// will be scheduled to repaint when:
///
/// * It's assigned to a new [RenderEditable] and the [shouldRepaint] method
/// returns true.
/// * Any of the [RenderEditable]s it is attached to repaints.
/// * The [notifyListeners] method is called, which typically happens when the
/// painter's attributes change.
///
/// See also:
///
/// * [RenderEditable.foregroundPainter], which takes a [RenderEditablePainter]
/// and sets it as the foreground painter of the [RenderEditable].
/// * [RenderEditable.painter], which takes a [RenderEditablePainter]
/// and sets it as the background painter of the [RenderEditable].
/// * [CustomPainter] a similar class which paints within a [RenderCustomPaint].
abstract class RenderEditablePainter extends ChangeNotifier {
/// Determines whether repaint is needed when a new [RenderEditablePainter]
/// is provided to a [RenderEditable].
///
/// If the new instance represents different information than the old
/// instance, then the method should return true, otherwise it should return
/// false. When [oldDelegate] is null, this method should always return true
/// unless the new painter initially does not paint anything.
///
/// If the method returns false, then the [paint] call might be optimized
/// away. However, the [paint] method will get called whenever the
/// [RenderEditable]s it attaches to repaint, even if [shouldRepaint] returns
/// false.
bool shouldRepaint(RenderEditablePainter? oldDelegate);
/// Paints within the bounds of a [RenderEditable].
///
/// The given [Canvas] has the same coordinate space as the [RenderEditable],
/// which may be different from the coordinate space the [RenderEditable]'s
/// [TextPainter] uses, when the text moves inside the [RenderEditable].
///
/// Paint operations performed outside of the region defined by the [canvas]'s
/// origin and the [size] parameter may get clipped, when [RenderEditable]'s
/// [RenderEditable.clipBehavior] is not [Clip.none].
void paint(Canvas canvas, Size size, RenderEditable renderEditable);
}
class _TextHighlightPainter extends RenderEditablePainter {
_TextHighlightPainter({
TextRange? highlightedRange,
Color? highlightColor
}) : _highlightedRange = highlightedRange,
_highlightColor = highlightColor;
final Paint highlightPaint = Paint();
Color? get highlightColor => _highlightColor;
Color? _highlightColor;
set highlightColor(Color? newValue) {
if (newValue == _highlightColor)
return;
_highlightColor = newValue;
notifyListeners();
}
TextRange? get highlightedRange => _highlightedRange;
TextRange? _highlightedRange;
set highlightedRange(TextRange? newValue) {
if (newValue == _highlightedRange)
return;
_highlightedRange = newValue;
notifyListeners();
}
/// Controls how tall the selection highlight boxes are computed to be.
///
/// See [ui.BoxHeightStyle] for details on available styles.
ui.BoxHeightStyle get selectionHeightStyle => _selectionHeightStyle;
ui.BoxHeightStyle _selectionHeightStyle = ui.BoxHeightStyle.tight;
set selectionHeightStyle(ui.BoxHeightStyle value) {
assert(value != null);
if (_selectionHeightStyle == value)
return;
_selectionHeightStyle = value;
notifyListeners();
}
/// Controls how wide the selection highlight boxes are computed to be.
///
/// See [ui.BoxWidthStyle] for details on available styles.
ui.BoxWidthStyle get selectionWidthStyle => _selectionWidthStyle;
ui.BoxWidthStyle _selectionWidthStyle = ui.BoxWidthStyle.tight;
set selectionWidthStyle(ui.BoxWidthStyle value) {
assert(value != null);
if (_selectionWidthStyle == value)
return;
_selectionWidthStyle = value;
notifyListeners();
}
@override
void paint(Canvas canvas, Size size, RenderEditable renderEditable) {
final TextRange? range = highlightedRange;
final Color? color = highlightColor;
if (range == null || color == null || range.isCollapsed) {
return;
}
highlightPaint.color = color;
final List<TextBox> boxes = renderEditable._textPainter.getBoxesForSelection(
TextSelection(baseOffset: range.start, extentOffset: range.end),
boxHeightStyle: selectionHeightStyle,
boxWidthStyle: selectionWidthStyle
);
for (final TextBox box in boxes)
canvas.drawRect(box.toRect().shift(renderEditable._paintOffset), highlightPaint);
}
@override
bool shouldRepaint(RenderEditablePainter? oldDelegate) {
if (identical(oldDelegate, this))
return false;
if (oldDelegate == null)
return highlightColor != null && highlightedRange != null;
return oldDelegate is! _TextHighlightPainter
|| oldDelegate.highlightColor != highlightColor
|| oldDelegate.highlightedRange != highlightedRange
|| oldDelegate.selectionHeightStyle != selectionHeightStyle
|| oldDelegate.selectionWidthStyle != selectionWidthStyle;
}
}
class _FloatingCursorPainter extends RenderEditablePainter {
_FloatingCursorPainter(this.caretPaintCallback);
bool get shouldPaint => _shouldPaint;
bool _shouldPaint = true;
set shouldPaint(bool value) {
if (shouldPaint == value)
return;
_shouldPaint = value;
notifyListeners();
}
CaretChangedHandler caretPaintCallback;
bool showRegularCaret = false;
final Paint caretPaint = Paint();
late final Paint floatingCursorPaint = Paint();
Color? get caretColor => _caretColor;
Color? _caretColor;
set caretColor(Color? value) {
if (caretColor?.value == value?.value)
return;
_caretColor = value;
notifyListeners();
}
Radius? get cursorRadius => _cursorRadius;
Radius? _cursorRadius;
set cursorRadius(Radius? value) {
if (_cursorRadius == value)
return;
_cursorRadius = value;
notifyListeners();
}
Offset get cursorOffset => _cursorOffset;
Offset _cursorOffset = Offset.zero;
set cursorOffset(Offset value) {
if (_cursorOffset == value)
return;
_cursorOffset = value;
notifyListeners();
}
Color? get backgroundCursorColor => _backgroundCursorColor;
Color? _backgroundCursorColor;
set backgroundCursorColor(Color? value) {
if (backgroundCursorColor?.value == value?.value)
return;
_backgroundCursorColor = value;
if (showRegularCaret)
notifyListeners();
}
Rect? get floatingCursorRect => _floatingCursorRect;
Rect? _floatingCursorRect;
set floatingCursorRect(Rect? value) {
if (_floatingCursorRect == value)
return;
_floatingCursorRect = value;
notifyListeners();
}
void paintRegularCursor(Canvas canvas, RenderEditable renderEditable, Color caretColor, TextPosition textPosition) {
final Rect caretPrototype = renderEditable._caretPrototype;
final Offset caretOffset = renderEditable._textPainter.getOffsetForCaret(textPosition, caretPrototype);
Rect caretRect = caretPrototype.shift(caretOffset + cursorOffset);
final double? caretHeight = renderEditable._textPainter.getFullHeightForCaret(textPosition, caretPrototype);
if (caretHeight != null) {
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
final double heightDiff = caretHeight - caretRect.height;
// Center the caret vertically along the text.
caretRect = Rect.fromLTWH(
caretRect.left,
caretRect.top + heightDiff / 2,
caretRect.width,
caretRect.height,
);
break;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
// Override the height to take the full height of the glyph at the TextPosition
// when not on iOS. iOS has special handling that creates a taller caret.
// TODO(garyq): See the TODO for _computeCaretPrototype().
caretRect = Rect.fromLTWH(
caretRect.left,
caretRect.top - _kCaretHeightOffset,
caretRect.width,
caretHeight,
);
break;
}
}
caretRect = caretRect.shift(renderEditable._paintOffset);
final Rect integralRect = caretRect.shift(renderEditable._snapToPhysicalPixel(caretRect.topLeft));
if (shouldPaint) {
final Radius? radius = cursorRadius;
caretPaint.color = caretColor;
if (radius == null) {
canvas.drawRect(integralRect, caretPaint);
} else {
final RRect caretRRect = RRect.fromRectAndRadius(integralRect, radius);
canvas.drawRRect(caretRRect, caretPaint);
}
}
caretPaintCallback(integralRect);
}
@override
void paint(Canvas canvas, Size size, RenderEditable renderEditable) {
// Compute the caret location even when `shouldPaint` is false.
assert(renderEditable != null);
final TextSelection? selection = renderEditable.selection;
// TODO(LongCatIsLooong): skip painting the caret when the selection is
// (-1, -1).
if (selection == null || !selection.isCollapsed)
return;
final Rect? floatingCursorRect = this.floatingCursorRect;
final Color? caretColor = floatingCursorRect == null
? this.caretColor
: showRegularCaret ? backgroundCursorColor : null;
final TextPosition caretTextPosition = floatingCursorRect == null
? selection.extent
: renderEditable._floatingCursorTextPosition;
if (caretColor != null) {
paintRegularCursor(canvas, renderEditable, caretColor, caretTextPosition);
}
final Color? floatingCursorColor = this.caretColor?.withOpacity(0.75);
// Floating Cursor.
if (floatingCursorRect == null || floatingCursorColor == null || !shouldPaint)
return;
canvas.drawRRect(
RRect.fromRectAndRadius(floatingCursorRect.shift(renderEditable._paintOffset), _kFloatingCaretRadius),
floatingCursorPaint..color = floatingCursorColor,
);
}
@override
bool shouldRepaint(RenderEditablePainter? oldDelegate) {
if (identical(this, oldDelegate))
return false;
if (oldDelegate == null)
return shouldPaint;
return oldDelegate is! _FloatingCursorPainter
|| oldDelegate.shouldPaint != shouldPaint
|| oldDelegate.showRegularCaret != showRegularCaret
|| oldDelegate.caretColor != caretColor
|| oldDelegate.cursorRadius != cursorRadius
|| oldDelegate.cursorOffset != cursorOffset
|| oldDelegate.backgroundCursorColor != backgroundCursorColor
|| oldDelegate.floatingCursorRect != floatingCursorRect;
}
}
class _CompositeRenderEditablePainter extends RenderEditablePainter {
_CompositeRenderEditablePainter({ required this.painters });
final List<RenderEditablePainter> painters;
@override
void addListener(VoidCallback listener) {
for (final RenderEditablePainter painter in painters)
painter.addListener(listener);
}
@override
void removeListener(VoidCallback listener) {
for (final RenderEditablePainter painter in painters)
painter.removeListener(listener);
}
@override
void paint(Canvas canvas, Size size, RenderEditable renderEditable) {
for (final RenderEditablePainter painter in painters)
painter.paint(canvas, size, renderEditable);
}
@override
bool shouldRepaint(RenderEditablePainter? oldDelegate) {
if (identical(oldDelegate, this))
return false;
if (oldDelegate is! _CompositeRenderEditablePainter || oldDelegate.painters.length != painters.length)
return true;
final Iterator<RenderEditablePainter> oldPainters = oldDelegate.painters.iterator;
final Iterator<RenderEditablePainter> newPainters = painters.iterator;
while (oldPainters.moveNext() && newPainters.moveNext())
if (newPainters.current.shouldRepaint(oldPainters.current))
return true;
return false;
}
}
...@@ -2640,7 +2640,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2640,7 +2640,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
cursorWidth: widget.cursorWidth, cursorWidth: widget.cursorWidth,
cursorHeight: widget.cursorHeight, cursorHeight: widget.cursorHeight,
cursorRadius: widget.cursorRadius, cursorRadius: widget.cursorRadius,
cursorOffset: widget.cursorOffset, cursorOffset: widget.cursorOffset ?? Offset.zero,
selectionHeightStyle: widget.selectionHeightStyle, selectionHeightStyle: widget.selectionHeightStyle,
selectionWidthStyle: widget.selectionWidthStyle, selectionWidthStyle: widget.selectionWidthStyle,
paintCursorAboveText: widget.paintCursorAboveText, paintCursorAboveText: widget.paintCursorAboveText,
...@@ -2724,7 +2724,7 @@ class _Editable extends LeafRenderObjectWidget { ...@@ -2724,7 +2724,7 @@ class _Editable extends LeafRenderObjectWidget {
required this.cursorWidth, required this.cursorWidth,
this.cursorHeight, this.cursorHeight,
this.cursorRadius, this.cursorRadius,
this.cursorOffset, required this.cursorOffset,
required this.paintCursorAboveText, required this.paintCursorAboveText,
this.selectionHeightStyle = ui.BoxHeightStyle.tight, this.selectionHeightStyle = ui.BoxHeightStyle.tight,
this.selectionWidthStyle = ui.BoxWidthStyle.tight, this.selectionWidthStyle = ui.BoxWidthStyle.tight,
...@@ -2772,7 +2772,7 @@ class _Editable extends LeafRenderObjectWidget { ...@@ -2772,7 +2772,7 @@ class _Editable extends LeafRenderObjectWidget {
final double cursorWidth; final double cursorWidth;
final double? cursorHeight; final double? cursorHeight;
final Radius? cursorRadius; final Radius? cursorRadius;
final Offset? cursorOffset; final Offset cursorOffset;
final bool paintCursorAboveText; final bool paintCursorAboveText;
final ui.BoxHeightStyle selectionHeightStyle; final ui.BoxHeightStyle selectionHeightStyle;
final ui.BoxWidthStyle selectionWidthStyle; final ui.BoxWidthStyle selectionWidthStyle;
......
...@@ -88,7 +88,7 @@ void main() { ...@@ -88,7 +88,7 @@ void main() {
expect( expect(
editable.toStringDeep(minLevel: DiagnosticLevel.info), editable.toStringDeep(minLevel: DiagnosticLevel.info),
equalsIgnoringHashCodes( equalsIgnoringHashCodes(
'RenderEditable#00000 NEEDS-LAYOUT NEEDS-PAINT DETACHED\n' 'RenderEditable#00000 NEEDS-LAYOUT NEEDS-PAINT NEEDS-COMPOSITING-BITS-UPDATE DETACHED\n'
' │ parentData: MISSING\n' ' │ parentData: MISSING\n'
' │ constraints: MISSING\n' ' │ constraints: MISSING\n'
' │ size: MISSING\n' ' │ size: MISSING\n'
...@@ -134,10 +134,12 @@ void main() { ...@@ -134,10 +134,12 @@ void main() {
offset: 0, offset: 0,
), ),
); );
editable.layout(BoxConstraints.loose(const Size(1000.0, 1000.0))); layout(editable, constraints: BoxConstraints.loose(const Size(500.0, 500.0)));
// Prepare for painting after layout.
pumpFrame(phase: EnginePhase.compositingBits);
expect( expect(
(Canvas canvas) => editable.paint(TestRecordingPaintingContext(canvas), Offset.zero), (Canvas canvas) => editable.paint(TestRecordingPaintingContext(canvas), Offset.zero),
paints..clipRect(rect: const Rect.fromLTRB(0.0, 0.0, 1000.0, 10.0)), paints..clipRect(rect: const Rect.fromLTRB(0.0, 0.0, 500.0, 10.0)),
); );
}); });
...@@ -169,6 +171,9 @@ void main() { ...@@ -169,6 +171,9 @@ void main() {
layout(editable); layout(editable);
editable.layout(BoxConstraints.loose(const Size(100, 100))); editable.layout(BoxConstraints.loose(const Size(100, 100)));
// Prepare for painting after layout.
pumpFrame(phase: EnginePhase.compositingBits);
expect( expect(
editable, editable,
// Draw no cursor by default. // Draw no cursor by default.
...@@ -176,7 +181,7 @@ void main() { ...@@ -176,7 +181,7 @@ void main() {
); );
editable.showCursor = showCursor; editable.showCursor = showCursor;
pumpFrame(); pumpFrame(phase: EnginePhase.compositingBits);
expect(editable, paints..rect( expect(editable, paints..rect(
color: const Color.fromARGB(0xFF, 0xFF, 0x00, 0x00), color: const Color.fromARGB(0xFF, 0xFF, 0x00, 0x00),
...@@ -187,7 +192,7 @@ void main() { ...@@ -187,7 +192,7 @@ void main() {
editable.cursorColor = const Color.fromARGB(0xFF, 0x00, 0x00, 0xFF); editable.cursorColor = const Color.fromARGB(0xFF, 0x00, 0x00, 0xFF);
editable.cursorWidth = 4; editable.cursorWidth = 4;
editable.cursorRadius = const Radius.circular(3); editable.cursorRadius = const Radius.circular(3);
pumpFrame(); pumpFrame(phase: EnginePhase.compositingBits);
expect(editable, paints..rrect( expect(editable, paints..rrect(
color: const Color.fromARGB(0xFF, 0x00, 0x00, 0xFF), color: const Color.fromARGB(0xFF, 0x00, 0x00, 0xFF),
...@@ -198,7 +203,7 @@ void main() { ...@@ -198,7 +203,7 @@ void main() {
)); ));
editable.textScaleFactor = 2; editable.textScaleFactor = 2;
pumpFrame(); pumpFrame(phase: EnginePhase.compositingBits);
// Now the caret height is much bigger due to the bigger font scale. // Now the caret height is much bigger due to the bigger font scale.
expect(editable, paints..rrect( expect(editable, paints..rrect(
...@@ -211,7 +216,7 @@ void main() { ...@@ -211,7 +216,7 @@ void main() {
// Can turn off caret. // Can turn off caret.
showCursor.value = false; showCursor.value = false;
pumpFrame(); pumpFrame(phase: EnginePhase.compositingBits);
expect(editable, paintsExactlyCountTimes(#drawRRect, 0)); expect(editable, paintsExactlyCountTimes(#drawRRect, 0));
}); });
...@@ -265,9 +270,8 @@ void main() { ...@@ -265,9 +270,8 @@ void main() {
), ),
); );
layout(editable); layout(editable, constraints: BoxConstraints.loose(const Size(100, 100)));
pumpFrame(phase: EnginePhase.compositingBits);
editable.layout(BoxConstraints.loose(const Size(100, 100)));
expect( expect(
editable, editable,
// Draw no cursor by default. // Draw no cursor by default.
...@@ -275,7 +279,7 @@ void main() { ...@@ -275,7 +279,7 @@ void main() {
); );
editable.showCursor = showCursor; editable.showCursor = showCursor;
pumpFrame(); pumpFrame(phase: EnginePhase.compositingBits);
expect(editable, paints..rect( expect(editable, paints..rect(
color: const Color.fromARGB(0xFF, 0xFF, 0x00, 0x00), color: const Color.fromARGB(0xFF, 0xFF, 0x00, 0x00),
...@@ -286,7 +290,7 @@ void main() { ...@@ -286,7 +290,7 @@ void main() {
editable.cursorColor = const Color.fromARGB(0xFF, 0x00, 0x00, 0xFF); editable.cursorColor = const Color.fromARGB(0xFF, 0x00, 0x00, 0xFF);
editable.cursorWidth = 4; editable.cursorWidth = 4;
editable.cursorRadius = const Radius.circular(3); editable.cursorRadius = const Radius.circular(3);
pumpFrame(); pumpFrame(phase: EnginePhase.compositingBits);
expect(editable, paints..rrect( expect(editable, paints..rrect(
color: const Color.fromARGB(0xFF, 0x00, 0x00, 0xFF), color: const Color.fromARGB(0xFF, 0x00, 0x00, 0xFF),
...@@ -297,7 +301,7 @@ void main() { ...@@ -297,7 +301,7 @@ void main() {
)); ));
editable.textScaleFactor = 2; editable.textScaleFactor = 2;
pumpFrame(); pumpFrame(phase: EnginePhase.compositingBits);
// Now the caret height is much bigger due to the bigger font scale. // Now the caret height is much bigger due to the bigger font scale.
expect(editable, paints..rrect( expect(editable, paints..rrect(
...@@ -310,7 +314,7 @@ void main() { ...@@ -310,7 +314,7 @@ void main() {
// Can turn off caret. // Can turn off caret.
showCursor.value = false; showCursor.value = false;
pumpFrame(); pumpFrame(phase: EnginePhase.compositingBits);
expect(editable, paintsExactlyCountTimes(#drawRRect, 0)); expect(editable, paintsExactlyCountTimes(#drawRRect, 0));
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61024 }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61024
...@@ -393,7 +397,7 @@ void main() { ...@@ -393,7 +397,7 @@ void main() {
expect(editable, paintsExactlyCountTimes(#drawRect, 1)); expect(editable, paintsExactlyCountTimes(#drawRect, 1));
editable.paintCursorAboveText = false; editable.paintCursorAboveText = false;
pumpFrame(); pumpFrame(phase: EnginePhase.compositingBits);
expect( expect(
editable, editable,
...@@ -501,7 +505,7 @@ void main() { ...@@ -501,7 +505,7 @@ void main() {
viewportOffset.correctBy(10); viewportOffset.correctBy(10);
pumpFrame(); pumpFrame(phase: EnginePhase.compositingBits);
expect( expect(
editable, editable,
...@@ -654,7 +658,9 @@ void main() { ...@@ -654,7 +658,9 @@ void main() {
promptRectColor: promptRectColor, promptRectColor: promptRectColor,
promptRectRange: const TextRange(start: 0, end: 1), promptRectRange: const TextRange(start: 0, end: 1),
); );
editable.layout(BoxConstraints.loose(const Size(1000.0, 1000.0)));
layout(editable, constraints: BoxConstraints.loose(const Size(1000.0, 1000.0)));
pumpFrame(phase: EnginePhase.compositingBits);
expect( expect(
(Canvas canvas) => editable.paint(TestRecordingPaintingContext(canvas), Offset.zero), (Canvas canvas) => editable.paint(TestRecordingPaintingContext(canvas), Offset.zero),
...@@ -664,9 +670,9 @@ void main() { ...@@ -664,9 +670,9 @@ void main() {
editable.promptRectColor = null; editable.promptRectColor = null;
editable.layout(BoxConstraints.loose(const Size(1000.0, 1000.0))); editable.layout(BoxConstraints.loose(const Size(1000.0, 1000.0)));
pumpFrame(); pumpFrame(phase: EnginePhase.compositingBits);
expect(editable.promptRectColor, promptRectColor); expect(editable.promptRectColor, null);
expect( expect(
(Canvas canvas) => editable.paint(TestRecordingPaintingContext(canvas), Offset.zero), (Canvas canvas) => editable.paint(TestRecordingPaintingContext(canvas), Offset.zero),
isNot(paints..rect(color: promptRectColor)), isNot(paints..rect(color: promptRectColor)),
...@@ -1658,4 +1664,261 @@ void main() { ...@@ -1658,4 +1664,261 @@ void main() {
expect(RenderEditable.previousCharacter(4, '0123👨‍👩‍👦2345'), 3); expect(RenderEditable.previousCharacter(4, '0123👨‍👩‍👦2345'), 3);
}); });
}); });
group('custom painters', () {
final TextSelectionDelegate delegate = FakeEditableTextState();
final _TestRenderEditable editable = _TestRenderEditable(
textDirection: TextDirection.ltr,
offset: ViewportOffset.zero(),
textSelectionDelegate: delegate,
text: const TextSpan(
text: 'test',
style: TextStyle(
height: 1.0,
fontSize: 10.0,
fontFamily: 'Ahem',
),
),
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
selection: const TextSelection.collapsed(
offset: 4,
affinity: TextAffinity.upstream,
),
);
setUp(() { EditableText.debugDeterministicCursor = true; });
tearDown(() {
EditableText.debugDeterministicCursor = false;
_TestRenderEditablePainter.paintHistory.clear();
editable.foregroundPainter = null;
editable.painter = null;
editable.paintCount = 0;
final AbstractNode? parent = editable.parent;
if (parent is RenderConstrainedBox)
parent.child = null;
});
test('paints in the correct order', () {
layout(editable, constraints: BoxConstraints.loose(const Size(100, 100)));
// Prepare for painting after layout.
// Foreground painter.
editable.foregroundPainter = _TestRenderEditablePainter();
pumpFrame(phase: EnginePhase.compositingBits);
expect(
(Canvas canvas) => editable.paint(TestRecordingPaintingContext(canvas), Offset.zero),
paints
..paragraph()
..rect(rect: const Rect.fromLTRB(1, 1, 1, 1), color: const Color(0x12345678)),
);
// Background painter.
editable.foregroundPainter = null;
editable.painter = _TestRenderEditablePainter();
expect(
(Canvas canvas) => editable.paint(TestRecordingPaintingContext(canvas), Offset.zero),
paints
..rect(rect: const Rect.fromLTRB(1, 1, 1, 1), color: const Color(0x12345678))
..paragraph(),
);
editable.foregroundPainter = _TestRenderEditablePainter();
editable.painter = _TestRenderEditablePainter();
expect(
(Canvas canvas) => editable.paint(TestRecordingPaintingContext(canvas), Offset.zero),
paints
..rect(rect: const Rect.fromLTRB(1, 1, 1, 1), color: const Color(0x12345678))
..paragraph()
..rect(rect: const Rect.fromLTRB(1, 1, 1, 1), color: const Color(0x12345678)),
);
});
test('changing foreground painter', () {
layout(editable, constraints: BoxConstraints.loose(const Size(100, 100)));
// Prepare for painting after layout.
_TestRenderEditablePainter currentPainter = _TestRenderEditablePainter();
// Foreground painter.
editable.foregroundPainter = currentPainter;
pumpFrame(phase: EnginePhase.paint);
expect(currentPainter.paintCount, 1);
editable.foregroundPainter = (currentPainter = _TestRenderEditablePainter()..repaint = false);
pumpFrame(phase: EnginePhase.paint);
expect(currentPainter.paintCount, 0);
editable.foregroundPainter = (currentPainter = _TestRenderEditablePainter()..repaint = true);
pumpFrame(phase: EnginePhase.paint);
expect(currentPainter.paintCount, 1);
});
test('changing background painter', () {
layout(editable, constraints: BoxConstraints.loose(const Size(100, 100)));
// Prepare for painting after layout.
_TestRenderEditablePainter currentPainter = _TestRenderEditablePainter();
// Foreground painter.
editable.painter = currentPainter;
pumpFrame(phase: EnginePhase.paint);
expect(currentPainter.paintCount, 1);
editable.painter = (currentPainter = _TestRenderEditablePainter()..repaint = false);
pumpFrame(phase: EnginePhase.paint);
expect(currentPainter.paintCount, 0);
editable.painter = (currentPainter = _TestRenderEditablePainter()..repaint = true);
pumpFrame(phase: EnginePhase.paint);
expect(currentPainter.paintCount, 1);
});
test('swapping painters', () {
layout(editable, constraints: BoxConstraints.loose(const Size(100, 100)));
final _TestRenderEditablePainter painter1 = _TestRenderEditablePainter();
final _TestRenderEditablePainter painter2 = _TestRenderEditablePainter();
editable.painter = painter1;
editable.foregroundPainter = painter2;
pumpFrame(phase: EnginePhase.paint);
expect(
_TestRenderEditablePainter.paintHistory,
<_TestRenderEditablePainter>[painter1, painter2],
);
_TestRenderEditablePainter.paintHistory.clear();
editable.painter = painter2;
editable.foregroundPainter = painter1;
pumpFrame(phase: EnginePhase.paint);
expect(
_TestRenderEditablePainter.paintHistory,
<_TestRenderEditablePainter>[painter2, painter1],
);
});
test('reusing the same painter', () {
layout(editable, constraints: BoxConstraints.loose(const Size(100, 100)));
final _TestRenderEditablePainter painter = _TestRenderEditablePainter();
FlutterErrorDetails? errorDetails;
editable.painter = painter;
editable.foregroundPainter = painter;
pumpFrame(phase: EnginePhase.paint, onErrors: () {
errorDetails = renderer.takeFlutterErrorDetails();
});
expect(errorDetails, isNull);
expect(
_TestRenderEditablePainter.paintHistory,
<_TestRenderEditablePainter>[painter, painter],
);
expect(
(Canvas canvas) => editable.paint(TestRecordingPaintingContext(canvas), Offset.zero),
paints
..rect(rect: const Rect.fromLTRB(1, 1, 1, 1), color: const Color(0x12345678))
..paragraph()
..rect(rect: const Rect.fromLTRB(1, 1, 1, 1), color: const Color(0x12345678)),
);
});
test('does not repaint the render editable when custom painters need repaint', () {
layout(editable, constraints: BoxConstraints.loose(const Size(100, 100)));
final _TestRenderEditablePainter painter = _TestRenderEditablePainter();
editable.painter = painter;
pumpFrame(phase: EnginePhase.paint);
editable.paintCount = 0;
painter.paintCount = 0;
painter.markNeedsPaint();
pumpFrame(phase: EnginePhase.paint);
expect(editable.paintCount, 0);
expect(painter.paintCount, 1);
});
test('repaints when its RenderEditable repaints', () {
layout(editable, constraints: BoxConstraints.loose(const Size(100, 100)));
final _TestRenderEditablePainter painter = _TestRenderEditablePainter();
editable.painter = painter;
pumpFrame(phase: EnginePhase.paint);
editable.paintCount = 0;
painter.paintCount = 0;
editable.markNeedsPaint();
pumpFrame(phase: EnginePhase.paint);
expect(editable.paintCount, 1);
expect(painter.paintCount, 1);
});
test('correct coordinate space', () {
layout(editable, constraints: BoxConstraints.loose(const Size(100, 100)));
final _TestRenderEditablePainter painter = _TestRenderEditablePainter();
editable.painter = painter;
editable.offset = ViewportOffset.fixed(1000);
pumpFrame(phase: EnginePhase.compositingBits);
expect(
(Canvas canvas) => editable.paint(TestRecordingPaintingContext(canvas), Offset.zero),
paints
..rect(rect: const Rect.fromLTRB(1, 1, 1, 1), color: const Color(0x12345678))
..paragraph()
);
});
});
}
class _TestRenderEditable extends RenderEditable {
_TestRenderEditable({
required TextDirection textDirection,
required ViewportOffset offset,
required TextSelectionDelegate textSelectionDelegate,
TextSpan? text,
required LayerLink startHandleLayerLink,
required LayerLink endHandleLayerLink,
TextSelection? selection,
}) : super(
textDirection: textDirection,
offset: offset,
textSelectionDelegate: textSelectionDelegate,
text: text,
startHandleLayerLink: startHandleLayerLink,
endHandleLayerLink: endHandleLayerLink,
selection: selection,
);
int paintCount = 0;
@override
void paint(PaintingContext context, Offset offset) {
super.paint(context, offset);
paintCount += 1;
}
}
class _TestRenderEditablePainter extends RenderEditablePainter {
bool repaint = true;
int paintCount = 0;
static final List<_TestRenderEditablePainter> paintHistory = <_TestRenderEditablePainter>[];
@override
void paint(Canvas canvas, Size size, RenderEditable renderEditable) {
paintCount += 1;
canvas.drawRect(const Rect.fromLTRB(1, 1, 1, 1), Paint()..color = const Color(0x12345678));
paintHistory.add(this);
}
@override
bool shouldRepaint(RenderEditablePainter? oldDelegate) => repaint;
void markNeedsPaint() {
notifyListeners();
}
} }
...@@ -5792,7 +5792,7 @@ void main() { ...@@ -5792,7 +5792,7 @@ void main() {
await tester.pump(); await tester.pump();
// Nothing called when only the remote changes. // Nothing called when only the remote changes.
expect(log.length, 0); expect(log, isEmpty);
controller.clear(); controller.clear();
......
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