1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:collection';
6import 'dart:math' as math;
7import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle, LineMetrics, PlaceholderAlignment, TextBox;
8
9import 'package:characters/characters.dart';
10import 'package:flutter/foundation.dart';
11import 'package:flutter/gestures.dart';
12import 'package:flutter/semantics.dart';
13import 'package:flutter/services.dart';
14
15import 'box.dart';
16import 'custom_paint.dart';
17import 'layer.dart';
18import 'layout_helper.dart';
19import 'object.dart';
20import 'paragraph.dart';
21import 'viewport_offset.dart';
22
23const double _kCaretGap = 1.0; // pixels
24const double _kCaretHeightOffset = 2.0; // pixels
25
26// The additional size on the x and y axis with which to expand the prototype
27// cursor to render the floating cursor in pixels.
28const EdgeInsets _kFloatingCursorSizeIncrease = EdgeInsets.symmetric(horizontal: 0.5, vertical: 1.0);
29
30// The corner radius of the floating cursor in pixels.
31const Radius _kFloatingCursorRadius = Radius.circular(1.0);
32
33// This constant represents the shortest squared distance required between the floating cursor
34// and the regular cursor when both are present in the text field.
35// If the squared distance between the two cursors is less than this value,
36// it's not necessary to display both cursors at the same time.
37// This behavior is consistent with the one observed in iOS UITextField.
38const double _kShortestDistanceSquaredWithFloatingAndRegularCursors = 15.0 * 15.0;
39
40/// Represents the coordinates of the point in a selection, and the text
41/// direction at that point, relative to top left of the [RenderEditable] that
42/// holds the selection.
43@immutable
44class TextSelectionPoint {
45 /// Creates a description of a point in a text selection.
46 const TextSelectionPoint(this.point, this.direction);
47
48 /// Coordinates of the lower left or lower right corner of the selection,
49 /// relative to the top left of the [RenderEditable] object.
50 final Offset point;
51
52 /// Direction of the text at this edge of the selection.
53 final TextDirection? direction;
54
55 @override
56 bool operator ==(Object other) {
57 if (identical(this, other)) {
58 return true;
59 }
60 if (other.runtimeType != runtimeType) {
61 return false;
62 }
63 return other is TextSelectionPoint
64 && other.point == point
65 && other.direction == direction;
66 }
67
68 @override
69 String toString() {
70 switch (direction) {
71 case TextDirection.ltr:
72 return '$point-ltr';
73 case TextDirection.rtl:
74 return '$point-rtl';
75 case null:
76 return '$point';
77 }
78 }
79
80 @override
81 int get hashCode => Object.hash(point, direction);
82
83}
84
85/// The consecutive sequence of [TextPosition]s that the caret should move to
86/// when the user navigates the paragraph using the upward arrow key or the
87/// downward arrow key.
88///
89/// {@template flutter.rendering.RenderEditable.verticalArrowKeyMovement}
90/// When the user presses the upward arrow key or the downward arrow key, on
91/// many platforms (macOS for instance), the caret will move to the previous
92/// line or the next line, while maintaining its original horizontal location.
93/// When it encounters a shorter line, the caret moves to the closest horizontal
94/// location within that line, and restores the original horizontal location
95/// when a long enough line is encountered.
96///
97/// Additionally, the caret will move to the beginning of the document if the
98/// upward arrow key is pressed and the caret is already on the first line. If
99/// the downward arrow key is pressed next, the caret will restore its original
100/// horizontal location and move to the second line. Similarly the caret moves
101/// to the end of the document if the downward arrow key is pressed when it's
102/// already on the last line.
103///
104/// Consider a left-aligned paragraph:
105/// aa|
106/// a
107/// aaa
108/// where the caret was initially placed at the end of the first line. Pressing
109/// the downward arrow key once will move the caret to the end of the second
110/// line, and twice the arrow key moves to the third line after the second "a"
111/// on that line. Pressing the downward arrow key again, the caret will move to
112/// the end of the third line (the end of the document). Pressing the upward
113/// arrow key in this state will result in the caret moving to the end of the
114/// second line.
115///
116/// Vertical caret runs are typically interrupted when the layout of the text
117/// changes (including when the text itself changes), or when the selection is
118/// changed by other input events or programmatically (for example, when the
119/// user pressed the left arrow key).
120/// {@endtemplate}
121///
122/// The [movePrevious] method moves the caret location (which is
123/// [VerticalCaretMovementRun.current]) to the previous line, and in case
124/// the caret is already on the first line, the method does nothing and returns
125/// false. Similarly the [moveNext] method moves the caret to the next line, and
126/// returns false if the caret is already on the last line.
127///
128/// The [moveByOffset] method takes a pixel offset from the current position to move
129/// the caret up or down.
130///
131/// If the underlying paragraph's layout changes, [isValid] becomes false and
132/// the [VerticalCaretMovementRun] must not be used. The [isValid] property must
133/// be checked before calling [movePrevious], [moveNext] and [moveByOffset],
134/// or accessing [current].
135class VerticalCaretMovementRun implements Iterator<TextPosition> {
136 VerticalCaretMovementRun._(
137 this._editable,
138 this._lineMetrics,
139 this._currentTextPosition,
140 this._currentLine,
141 this._currentOffset,
142 );
143
144 Offset _currentOffset;
145 int _currentLine;
146 TextPosition _currentTextPosition;
147
148 final List<ui.LineMetrics> _lineMetrics;
149 final RenderEditable _editable;
150
151 bool _isValid = true;
152 /// Whether this [VerticalCaretMovementRun] can still continue.
153 ///
154 /// A [VerticalCaretMovementRun] run is valid if the underlying text layout
155 /// hasn't changed.
156 ///
157 /// The [current] value and the [movePrevious], [moveNext] and [moveByOffset]
158 /// methods must not be accessed when [isValid] is false.
159 bool get isValid {
160 if (!_isValid) {
161 return false;
162 }
163 final List<ui.LineMetrics> newLineMetrics = _editable._textPainter.computeLineMetrics();
164 // Use the implementation detail of the computeLineMetrics method to figure
165 // out if the current text layout has been invalidated.
166 if (!identical(newLineMetrics, _lineMetrics)) {
167 _isValid = false;
168 }
169 return _isValid;
170 }
171
172 final Map<int, MapEntry<Offset, TextPosition>> _positionCache = <int, MapEntry<Offset, TextPosition>>{};
173
174 MapEntry<Offset, TextPosition> _getTextPositionForLine(int lineNumber) {
175 assert(isValid);
176 assert(lineNumber >= 0);
177 final MapEntry<Offset, TextPosition>? cachedPosition = _positionCache[lineNumber];
178 if (cachedPosition != null) {
179 return cachedPosition;
180 }
181 assert(lineNumber != _currentLine);
182
183 final Offset newOffset = Offset(_currentOffset.dx, _lineMetrics[lineNumber].baseline);
184 final TextPosition closestPosition = _editable._textPainter.getPositionForOffset(newOffset);
185 final MapEntry<Offset, TextPosition> position = MapEntry<Offset, TextPosition>(newOffset, closestPosition);
186 _positionCache[lineNumber] = position;
187 return position;
188 }
189
190 @override
191 TextPosition get current {
192 assert(isValid);
193 return _currentTextPosition;
194 }
195
196 @override
197 bool moveNext() {
198 assert(isValid);
199 if (_currentLine + 1 >= _lineMetrics.length) {
200 return false;
201 }
202 final MapEntry<Offset, TextPosition> position = _getTextPositionForLine(_currentLine + 1);
203 _currentLine += 1;
204 _currentOffset = position.key;
205 _currentTextPosition = position.value;
206 return true;
207 }
208
209 /// Move back to the previous element.
210 ///
211 /// Returns true and updates [current] if successful.
212 bool movePrevious() {
213 assert(isValid);
214 if (_currentLine <= 0) {
215 return false;
216 }
217 final MapEntry<Offset, TextPosition> position = _getTextPositionForLine(_currentLine - 1);
218 _currentLine -= 1;
219 _currentOffset = position.key;
220 _currentTextPosition = position.value;
221 return true;
222 }
223
224 /// Move forward or backward by a number of elements determined
225 /// by pixel [offset].
226 ///
227 /// If [offset] is negative, move backward; otherwise move forward.
228 ///
229 /// Returns true and updates [current] if successful.
230 bool moveByOffset(double offset) {
231 final Offset initialOffset = _currentOffset;
232 if (offset >= 0.0) {
233 while (_currentOffset.dy < initialOffset.dy + offset) {
234 if (!moveNext()) {
235 break;
236 }
237 }
238 } else {
239 while (_currentOffset.dy > initialOffset.dy + offset) {
240 if (!movePrevious()) {
241 break;
242 }
243 }
244 }
245 return initialOffset != _currentOffset;
246 }
247}
248
249/// Displays some text in a scrollable container with a potentially blinking
250/// cursor and with gesture recognizers.
251///
252/// This is the renderer for an editable text field. It does not directly
253/// provide affordances for editing the text, but it does handle text selection
254/// and manipulation of the text cursor.
255///
256/// The [text] is displayed, scrolled by the given [offset], aligned according
257/// to [textAlign]. The [maxLines] property controls whether the text displays
258/// on one line or many. The [selection], if it is not collapsed, is painted in
259/// the [selectionColor]. If it _is_ collapsed, then it represents the cursor
260/// position. The cursor is shown while [showCursor] is true. It is painted in
261/// the [cursorColor].
262///
263/// Keyboard handling, IME handling, scrolling, toggling the [showCursor] value
264/// to actually blink the cursor, and other features not mentioned above are the
265/// responsibility of higher layers and not handled by this object.
266class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, ContainerRenderObjectMixin<RenderBox, TextParentData>, RenderInlineChildrenContainerDefaults implements TextLayoutMetrics {
267 /// Creates a render object that implements the visual aspects of a text field.
268 ///
269 /// The [textAlign] argument defaults to [TextAlign.start].
270 ///
271 /// If [showCursor] is not specified, then it defaults to hiding the cursor.
272 ///
273 /// The [maxLines] property can be set to null to remove the restriction on
274 /// the number of lines. By default, it is 1, meaning this is a single-line
275 /// text field. If it is not null, it must be greater than zero.
276 ///
277 /// Use [ViewportOffset.zero] for the [offset] if there is no need for
278 /// scrolling.
279 RenderEditable({
280 InlineSpan? text,
281 required TextDirection textDirection,
282 TextAlign textAlign = TextAlign.start,
283 Color? cursorColor,
284 Color? backgroundCursorColor,
285 ValueNotifier<bool>? showCursor,
286 bool? hasFocus,
287 required LayerLink startHandleLayerLink,
288 required LayerLink endHandleLayerLink,
289 int? maxLines = 1,
290 int? minLines,
291 bool expands = false,
292 StrutStyle? strutStyle,
293 Color? selectionColor,
294 @Deprecated(
295 'Use textScaler instead. '
296 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
297 'This feature was deprecated after v3.12.0-2.0.pre.',
298 )
299 double textScaleFactor = 1.0,
300 TextScaler textScaler = TextScaler.noScaling,
301 TextSelection? selection,
302 required ViewportOffset offset,
303 this.ignorePointer = false,
304 bool readOnly = false,
305 bool forceLine = true,
306 TextHeightBehavior? textHeightBehavior,
307 TextWidthBasis textWidthBasis = TextWidthBasis.parent,
308 String obscuringCharacter = '•',
309 bool obscureText = false,
310 Locale? locale,
311 double cursorWidth = 1.0,
312 double? cursorHeight,
313 Radius? cursorRadius,
314 bool paintCursorAboveText = false,
315 Offset cursorOffset = Offset.zero,
316 double devicePixelRatio = 1.0,
317 ui.BoxHeightStyle selectionHeightStyle = ui.BoxHeightStyle.tight,
318 ui.BoxWidthStyle selectionWidthStyle = ui.BoxWidthStyle.tight,
319 bool? enableInteractiveSelection,
320 this.floatingCursorAddedMargin = const EdgeInsets.fromLTRB(4, 4, 4, 5),
321 TextRange? promptRectRange,
322 Color? promptRectColor,
323 Clip clipBehavior = Clip.hardEdge,
324 required this.textSelectionDelegate,
325 RenderEditablePainter? painter,
326 RenderEditablePainter? foregroundPainter,
327 List<RenderBox>? children,
328 }) : assert(maxLines == null || maxLines > 0),
329 assert(minLines == null || minLines > 0),
330 assert(
331 (maxLines == null) || (minLines == null) || (maxLines >= minLines),
332 "minLines can't be greater than maxLines",
333 ),
334 assert(
335 !expands || (maxLines == null && minLines == null),
336 'minLines and maxLines must be null when expands is true.',
337 ),
338 assert(
339 identical(textScaler, TextScaler.noScaling) || textScaleFactor == 1.0,
340 'textScaleFactor is deprecated and cannot be specified when textScaler is specified.',
341 ),
342 assert(obscuringCharacter.characters.length == 1),
343 assert(cursorWidth >= 0.0),
344 assert(cursorHeight == null || cursorHeight >= 0.0),
345 _textPainter = TextPainter(
346 text: text,
347 textAlign: textAlign,
348 textDirection: textDirection,
349 textScaler: textScaler == TextScaler.noScaling ? TextScaler.linear(textScaleFactor) : textScaler,
350 locale: locale,
351 maxLines: maxLines == 1 ? 1 : null,
352 strutStyle: strutStyle,
353 textHeightBehavior: textHeightBehavior,
354 textWidthBasis: textWidthBasis,
355 ),
356 _showCursor = showCursor ?? ValueNotifier<bool>(false),
357 _maxLines = maxLines,
358 _minLines = minLines,
359 _expands = expands,
360 _selection = selection,
361 _offset = offset,
362 _cursorWidth = cursorWidth,
363 _cursorHeight = cursorHeight,
364 _paintCursorOnTop = paintCursorAboveText,
365 _enableInteractiveSelection = enableInteractiveSelection,
366 _devicePixelRatio = devicePixelRatio,
367 _startHandleLayerLink = startHandleLayerLink,
368 _endHandleLayerLink = endHandleLayerLink,
369 _obscuringCharacter = obscuringCharacter,
370 _obscureText = obscureText,
371 _readOnly = readOnly,
372 _forceLine = forceLine,
373 _clipBehavior = clipBehavior,
374 _hasFocus = hasFocus ?? false,
375 _disposeShowCursor = showCursor == null {
376 assert(!_showCursor.value || cursorColor != null);
377
378 _selectionPainter.highlightColor = selectionColor;
379 _selectionPainter.highlightedRange = selection;
380 _selectionPainter.selectionHeightStyle = selectionHeightStyle;
381 _selectionPainter.selectionWidthStyle = selectionWidthStyle;
382
383 _autocorrectHighlightPainter.highlightColor = promptRectColor;
384 _autocorrectHighlightPainter.highlightedRange = promptRectRange;
385
386 _caretPainter.caretColor = cursorColor;
387 _caretPainter.cursorRadius = cursorRadius;
388 _caretPainter.cursorOffset = cursorOffset;
389 _caretPainter.backgroundCursorColor = backgroundCursorColor;
390
391 _updateForegroundPainter(foregroundPainter);
392 _updatePainter(painter);
393 addAll(children);
394 }
395
396 /// Child render objects
397 _RenderEditableCustomPaint? _foregroundRenderObject;
398 _RenderEditableCustomPaint? _backgroundRenderObject;
399
400 @override
401 void dispose() {
402 _leaderLayerHandler.layer = null;
403 _foregroundRenderObject?.dispose();
404 _foregroundRenderObject = null;
405 _backgroundRenderObject?.dispose();
406 _backgroundRenderObject = null;
407 _clipRectLayer.layer = null;
408 _cachedBuiltInForegroundPainters?.dispose();
409 _cachedBuiltInPainters?.dispose();
410 _selectionStartInViewport.dispose();
411 _selectionEndInViewport.dispose();
412 _autocorrectHighlightPainter.dispose();
413 _selectionPainter.dispose();
414 _caretPainter.dispose();
415 _textPainter.dispose();
416 if (_disposeShowCursor) {
417 _showCursor.dispose();
418 _disposeShowCursor = false;
419 }
420 super.dispose();
421 }
422
423 void _updateForegroundPainter(RenderEditablePainter? newPainter) {
424 final _CompositeRenderEditablePainter effectivePainter = newPainter == null
425 ? _builtInForegroundPainters
426 : _CompositeRenderEditablePainter(painters: <RenderEditablePainter>[
427 _builtInForegroundPainters,
428 newPainter,
429 ]);
430
431 if (_foregroundRenderObject == null) {
432 final _RenderEditableCustomPaint foregroundRenderObject = _RenderEditableCustomPaint(painter: effectivePainter);
433 adoptChild(foregroundRenderObject);
434 _foregroundRenderObject = foregroundRenderObject;
435 } else {
436 _foregroundRenderObject?.painter = effectivePainter;
437 }
438 _foregroundPainter = newPainter;
439 }
440
441 /// The [RenderEditablePainter] to use for painting above this
442 /// [RenderEditable]'s text content.
443 ///
444 /// The new [RenderEditablePainter] will replace the previously specified
445 /// foreground painter, and schedule a repaint if the new painter's
446 /// `shouldRepaint` method returns true.
447 RenderEditablePainter? get foregroundPainter => _foregroundPainter;
448 RenderEditablePainter? _foregroundPainter;
449 set foregroundPainter(RenderEditablePainter? newPainter) {
450 if (newPainter == _foregroundPainter) {
451 return;
452 }
453 _updateForegroundPainter(newPainter);
454 }
455
456 void _updatePainter(RenderEditablePainter? newPainter) {
457 final _CompositeRenderEditablePainter effectivePainter = newPainter == null
458 ? _builtInPainters
459 : _CompositeRenderEditablePainter(painters: <RenderEditablePainter>[_builtInPainters, newPainter]);
460
461 if (_backgroundRenderObject == null) {
462 final _RenderEditableCustomPaint backgroundRenderObject = _RenderEditableCustomPaint(painter: effectivePainter);
463 adoptChild(backgroundRenderObject);
464 _backgroundRenderObject = backgroundRenderObject;
465 } else {
466 _backgroundRenderObject?.painter = effectivePainter;
467 }
468 _painter = newPainter;
469 }
470
471 /// Sets the [RenderEditablePainter] to use for painting beneath this
472 /// [RenderEditable]'s text content.
473 ///
474 /// The new [RenderEditablePainter] will replace the previously specified
475 /// painter, and schedule a repaint if the new painter's `shouldRepaint`
476 /// method returns true.
477 RenderEditablePainter? get painter => _painter;
478 RenderEditablePainter? _painter;
479 set painter(RenderEditablePainter? newPainter) {
480 if (newPainter == _painter) {
481 return;
482 }
483 _updatePainter(newPainter);
484 }
485
486 // Caret Painters:
487 // A single painter for both the regular caret and the floating cursor.
488 late final _CaretPainter _caretPainter = _CaretPainter();
489
490 // Text Highlight painters:
491 final _TextHighlightPainter _selectionPainter = _TextHighlightPainter();
492 final _TextHighlightPainter _autocorrectHighlightPainter = _TextHighlightPainter();
493
494 _CompositeRenderEditablePainter get _builtInForegroundPainters => _cachedBuiltInForegroundPainters ??= _createBuiltInForegroundPainters();
495 _CompositeRenderEditablePainter? _cachedBuiltInForegroundPainters;
496 _CompositeRenderEditablePainter _createBuiltInForegroundPainters() {
497 return _CompositeRenderEditablePainter(
498 painters: <RenderEditablePainter>[
499 if (paintCursorAboveText) _caretPainter,
500 ],
501 );
502 }
503
504 _CompositeRenderEditablePainter get _builtInPainters => _cachedBuiltInPainters ??= _createBuiltInPainters();
505 _CompositeRenderEditablePainter? _cachedBuiltInPainters;
506 _CompositeRenderEditablePainter _createBuiltInPainters() {
507 return _CompositeRenderEditablePainter(
508 painters: <RenderEditablePainter>[
509 _autocorrectHighlightPainter,
510 _selectionPainter,
511 if (!paintCursorAboveText) _caretPainter,
512 ],
513 );
514 }
515
516 double? _textLayoutLastMaxWidth;
517 double? _textLayoutLastMinWidth;
518
519 /// Assert that the last layout still matches the constraints.
520 void debugAssertLayoutUpToDate() {
521 assert(
522 _textLayoutLastMaxWidth == constraints.maxWidth &&
523 _textLayoutLastMinWidth == constraints.minWidth,
524 'Last width ($_textLayoutLastMinWidth, $_textLayoutLastMaxWidth) not the same as max width constraint (${constraints.minWidth}, ${constraints.maxWidth}).',
525 );
526 }
527
528 /// Whether the [handleEvent] will propagate pointer events to selection
529 /// handlers.
530 ///
531 /// If this property is true, the [handleEvent] assumes that this renderer
532 /// will be notified of input gestures via [handleTapDown], [handleTap],
533 /// [handleDoubleTap], and [handleLongPress].
534 ///
535 /// If there are any gesture recognizers in the text span, the [handleEvent]
536 /// will still propagate pointer events to those recognizers.
537 ///
538 /// The default value of this property is false.
539 bool ignorePointer;
540
541 /// {@macro dart.ui.textHeightBehavior}
542 TextHeightBehavior? get textHeightBehavior => _textPainter.textHeightBehavior;
543 set textHeightBehavior(TextHeightBehavior? value) {
544 if (_textPainter.textHeightBehavior == value) {
545 return;
546 }
547 _textPainter.textHeightBehavior = value;
548 markNeedsTextLayout();
549 }
550
551 /// {@macro flutter.painting.textPainter.textWidthBasis}
552 TextWidthBasis get textWidthBasis => _textPainter.textWidthBasis;
553 set textWidthBasis(TextWidthBasis value) {
554 if (_textPainter.textWidthBasis == value) {
555 return;
556 }
557 _textPainter.textWidthBasis = value;
558 markNeedsTextLayout();
559 }
560
561 /// The pixel ratio of the current device.
562 ///
563 /// Should be obtained by querying MediaQuery for the devicePixelRatio.
564 double get devicePixelRatio => _devicePixelRatio;
565 double _devicePixelRatio;
566 set devicePixelRatio(double value) {
567 if (devicePixelRatio == value) {
568 return;
569 }
570 _devicePixelRatio = value;
571 markNeedsTextLayout();
572 }
573
574 /// Character used for obscuring text if [obscureText] is true.
575 ///
576 /// Must have a length of exactly one.
577 String get obscuringCharacter => _obscuringCharacter;
578 String _obscuringCharacter;
579 set obscuringCharacter(String value) {
580 if (_obscuringCharacter == value) {
581 return;
582 }
583 assert(value.characters.length == 1);
584 _obscuringCharacter = value;
585 markNeedsLayout();
586 }
587
588 /// Whether to hide the text being edited (e.g., for passwords).
589 bool get obscureText => _obscureText;
590 bool _obscureText;
591 set obscureText(bool value) {
592 if (_obscureText == value) {
593 return;
594 }
595 _obscureText = value;
596 _cachedAttributedValue = null;
597 markNeedsSemanticsUpdate();
598 }
599
600 /// Controls how tall the selection highlight boxes are computed to be.
601 ///
602 /// See [ui.BoxHeightStyle] for details on available styles.
603 ui.BoxHeightStyle get selectionHeightStyle => _selectionPainter.selectionHeightStyle;
604 set selectionHeightStyle(ui.BoxHeightStyle value) {
605 _selectionPainter.selectionHeightStyle = value;
606 }
607
608 /// Controls how wide the selection highlight boxes are computed to be.
609 ///
610 /// See [ui.BoxWidthStyle] for details on available styles.
611 ui.BoxWidthStyle get selectionWidthStyle => _selectionPainter.selectionWidthStyle;
612 set selectionWidthStyle(ui.BoxWidthStyle value) {
613 _selectionPainter.selectionWidthStyle = value;
614 }
615
616 /// The object that controls the text selection, used by this render object
617 /// for implementing cut, copy, and paste keyboard shortcuts.
618 ///
619 /// It will make cut, copy and paste functionality work with the most recently
620 /// set [TextSelectionDelegate].
621 TextSelectionDelegate textSelectionDelegate;
622
623 /// Track whether position of the start of the selected text is within the viewport.
624 ///
625 /// For example, if the text contains "Hello World", and the user selects
626 /// "Hello", then scrolls so only "World" is visible, this will become false.
627 /// If the user scrolls back so that the "H" is visible again, this will
628 /// become true.
629 ///
630 /// This bool indicates whether the text is scrolled so that the handle is
631 /// inside the text field viewport, as opposed to whether it is actually
632 /// visible on the screen.
633 ValueListenable<bool> get selectionStartInViewport => _selectionStartInViewport;
634 final ValueNotifier<bool> _selectionStartInViewport = ValueNotifier<bool>(true);
635
636 /// Track whether position of the end of the selected text is within the viewport.
637 ///
638 /// For example, if the text contains "Hello World", and the user selects
639 /// "World", then scrolls so only "Hello" is visible, this will become
640 /// 'false'. If the user scrolls back so that the "d" is visible again, this
641 /// will become 'true'.
642 ///
643 /// This bool indicates whether the text is scrolled so that the handle is
644 /// inside the text field viewport, as opposed to whether it is actually
645 /// visible on the screen.
646 ValueListenable<bool> get selectionEndInViewport => _selectionEndInViewport;
647 final ValueNotifier<bool> _selectionEndInViewport = ValueNotifier<bool>(true);
648
649 /// Returns the TextPosition above or below the given offset.
650 TextPosition _getTextPositionVertical(TextPosition position, double verticalOffset) {
651 final Offset caretOffset = _textPainter.getOffsetForCaret(position, _caretPrototype);
652 final Offset caretOffsetTranslated = caretOffset.translate(0.0, verticalOffset);
653 return _textPainter.getPositionForOffset(caretOffsetTranslated);
654 }
655
656 // Start TextLayoutMetrics.
657
658 /// {@macro flutter.services.TextLayoutMetrics.getLineAtOffset}
659 @override
660 TextSelection getLineAtOffset(TextPosition position) {
661 debugAssertLayoutUpToDate();
662 final TextRange line = _textPainter.getLineBoundary(position);
663 // If text is obscured, the entire string should be treated as one line.
664 if (obscureText) {
665 return TextSelection(baseOffset: 0, extentOffset: plainText.length);
666 }
667 return TextSelection(baseOffset: line.start, extentOffset: line.end);
668 }
669
670 /// {@macro flutter.painting.TextPainter.getWordBoundary}
671 @override
672 TextRange getWordBoundary(TextPosition position) {
673 return _textPainter.getWordBoundary(position);
674 }
675
676 /// {@macro flutter.services.TextLayoutMetrics.getTextPositionAbove}
677 @override
678 TextPosition getTextPositionAbove(TextPosition position) {
679 // The caret offset gives a location in the upper left hand corner of
680 // the caret so the middle of the line above is a half line above that
681 // point and the line below is 1.5 lines below that point.
682 final double preferredLineHeight = _textPainter.preferredLineHeight;
683 final double verticalOffset = -0.5 * preferredLineHeight;
684 return _getTextPositionVertical(position, verticalOffset);
685 }
686
687 /// {@macro flutter.services.TextLayoutMetrics.getTextPositionBelow}
688 @override
689 TextPosition getTextPositionBelow(TextPosition position) {
690 // The caret offset gives a location in the upper left hand corner of
691 // the caret so the middle of the line above is a half line above that
692 // point and the line below is 1.5 lines below that point.
693 final double preferredLineHeight = _textPainter.preferredLineHeight;
694 final double verticalOffset = 1.5 * preferredLineHeight;
695 return _getTextPositionVertical(position, verticalOffset);
696 }
697
698 // End TextLayoutMetrics.
699
700 void _updateSelectionExtentsVisibility(Offset effectiveOffset) {
701 assert(selection != null);
702 if (!selection!.isValid) {
703 _selectionStartInViewport.value = false;
704 _selectionEndInViewport.value = false;
705 return;
706 }
707 final Rect visibleRegion = Offset.zero & size;
708
709 final Offset startOffset = _textPainter.getOffsetForCaret(
710 TextPosition(offset: selection!.start, affinity: selection!.affinity),
711 _caretPrototype,
712 );
713 // Check if the selection is visible with an approximation because a
714 // difference between rounded and unrounded values causes the caret to be
715 // reported as having a slightly (< 0.5) negative y offset. This rounding
716 // happens in paragraph.cc's layout and TextPainter's
717 // _applyFloatingPointHack. Ideally, the rounding mismatch will be fixed and
718 // this can be changed to be a strict check instead of an approximation.
719 const double visibleRegionSlop = 0.5;
720 _selectionStartInViewport.value = visibleRegion
721 .inflate(visibleRegionSlop)
722 .contains(startOffset + effectiveOffset);
723
724 final Offset endOffset = _textPainter.getOffsetForCaret(
725 TextPosition(offset: selection!.end, affinity: selection!.affinity),
726 _caretPrototype,
727 );
728 _selectionEndInViewport.value = visibleRegion
729 .inflate(visibleRegionSlop)
730 .contains(endOffset + effectiveOffset);
731 }
732
733 void _setTextEditingValue(TextEditingValue newValue, SelectionChangedCause cause) {
734 textSelectionDelegate.userUpdateTextEditingValue(newValue, cause);
735 }
736
737 void _setSelection(TextSelection nextSelection, SelectionChangedCause cause) {
738 if (nextSelection.isValid) {
739 // The nextSelection is calculated based on plainText, which can be out
740 // of sync with the textSelectionDelegate.textEditingValue by one frame.
741 // This is due to the render editable and editable text handle pointer
742 // event separately. If the editable text changes the text during the
743 // event handler, the render editable will use the outdated text stored in
744 // the plainText when handling the pointer event.
745 //
746 // If this happens, we need to make sure the new selection is still valid.
747 final int textLength = textSelectionDelegate.textEditingValue.text.length;
748 nextSelection = nextSelection.copyWith(
749 baseOffset: math.min(nextSelection.baseOffset, textLength),
750 extentOffset: math.min(nextSelection.extentOffset, textLength),
751 );
752 }
753 _setTextEditingValue(
754 textSelectionDelegate.textEditingValue.copyWith(selection: nextSelection),
755 cause,
756 );
757 }
758
759 @override
760 void markNeedsPaint() {
761 super.markNeedsPaint();
762 // Tell the painters to repaint since text layout may have changed.
763 _foregroundRenderObject?.markNeedsPaint();
764 _backgroundRenderObject?.markNeedsPaint();
765 }
766
767 /// Marks the render object as needing to be laid out again and have its text
768 /// metrics recomputed.
769 ///
770 /// Implies [markNeedsLayout].
771 @protected
772 void markNeedsTextLayout() {
773 _textLayoutLastMaxWidth = null;
774 _textLayoutLastMinWidth = null;
775 markNeedsLayout();
776 }
777
778 @override
779 void systemFontsDidChange() {
780 super.systemFontsDidChange();
781 _textPainter.markNeedsLayout();
782 _textLayoutLastMaxWidth = null;
783 _textLayoutLastMinWidth = null;
784 }
785
786 /// Returns a plain text version of the text in [TextPainter].
787 ///
788 /// If [obscureText] is true, returns the obscured text. See
789 /// [obscureText] and [obscuringCharacter].
790 /// In order to get the styled text as an [InlineSpan] tree, use [text].
791 String get plainText => _textPainter.plainText;
792
793 /// The text to paint in the form of a tree of [InlineSpan]s.
794 ///
795 /// In order to get the plain text representation, use [plainText].
796 InlineSpan? get text => _textPainter.text;
797 final TextPainter _textPainter;
798 AttributedString? _cachedAttributedValue;
799 List<InlineSpanSemanticsInformation>? _cachedCombinedSemanticsInfos;
800 set text(InlineSpan? value) {
801 if (_textPainter.text == value) {
802 return;
803 }
804 _cachedLineBreakCount = null;
805 _textPainter.text = value;
806 _cachedAttributedValue = null;
807 _cachedCombinedSemanticsInfos = null;
808 _canComputeIntrinsicsCached = null;
809 markNeedsTextLayout();
810 markNeedsSemanticsUpdate();
811 }
812
813 /// How the text should be aligned horizontally.
814 TextAlign get textAlign => _textPainter.textAlign;
815 set textAlign(TextAlign value) {
816 if (_textPainter.textAlign == value) {
817 return;
818 }
819 _textPainter.textAlign = value;
820 markNeedsTextLayout();
821 }
822
823 /// The directionality of the text.
824 ///
825 /// This decides how the [TextAlign.start], [TextAlign.end], and
826 /// [TextAlign.justify] values of [textAlign] are interpreted.
827 ///
828 /// This is also used to disambiguate how to render bidirectional text. For
829 /// example, if the [text] is an English phrase followed by a Hebrew phrase,
830 /// in a [TextDirection.ltr] context the English phrase will be on the left
831 /// and the Hebrew phrase to its right, while in a [TextDirection.rtl]
832 /// context, the English phrase will be on the right and the Hebrew phrase on
833 /// its left.
834 // TextPainter.textDirection is nullable, but it is set to a
835 // non-null value in the RenderEditable constructor and we refuse to
836 // set it to null here, so _textPainter.textDirection cannot be null.
837 TextDirection get textDirection => _textPainter.textDirection!;
838 set textDirection(TextDirection value) {
839 if (_textPainter.textDirection == value) {
840 return;
841 }
842 _textPainter.textDirection = value;
843 markNeedsTextLayout();
844 markNeedsSemanticsUpdate();
845 }
846
847 /// Used by this renderer's internal [TextPainter] to select a locale-specific
848 /// font.
849 ///
850 /// In some cases the same Unicode character may be rendered differently depending
851 /// on the locale. For example the '骨' character is rendered differently in
852 /// the Chinese and Japanese locales. In these cases the [locale] may be used
853 /// to select a locale-specific font.
854 ///
855 /// If this value is null, a system-dependent algorithm is used to select
856 /// the font.
857 Locale? get locale => _textPainter.locale;
858 set locale(Locale? value) {
859 if (_textPainter.locale == value) {
860 return;
861 }
862 _textPainter.locale = value;
863 markNeedsTextLayout();
864 }
865
866 /// The [StrutStyle] used by the renderer's internal [TextPainter] to
867 /// determine the strut to use.
868 StrutStyle? get strutStyle => _textPainter.strutStyle;
869 set strutStyle(StrutStyle? value) {
870 if (_textPainter.strutStyle == value) {
871 return;
872 }
873 _textPainter.strutStyle = value;
874 markNeedsTextLayout();
875 }
876
877 /// The color to use when painting the cursor.
878 Color? get cursorColor => _caretPainter.caretColor;
879 set cursorColor(Color? value) {
880 _caretPainter.caretColor = value;
881 }
882
883 /// The color to use when painting the cursor aligned to the text while
884 /// rendering the floating cursor.
885 ///
886 /// Typically this would be set to [CupertinoColors.inactiveGray].
887 ///
888 /// If this is null, the background cursor is not painted.
889 Color? get backgroundCursorColor => _caretPainter.backgroundCursorColor;
890 set backgroundCursorColor(Color? value) {
891 _caretPainter.backgroundCursorColor = value;
892 }
893
894 bool _disposeShowCursor;
895
896 /// Whether to paint the cursor.
897 ValueNotifier<bool> get showCursor => _showCursor;
898 ValueNotifier<bool> _showCursor;
899 set showCursor(ValueNotifier<bool> value) {
900 if (_showCursor == value) {
901 return;
902 }
903 if (attached) {
904 _showCursor.removeListener(_showHideCursor);
905 }
906 if (_disposeShowCursor) {
907 _showCursor.dispose();
908 _disposeShowCursor = false;
909 }
910 _showCursor = value;
911 if (attached) {
912 _showHideCursor();
913 _showCursor.addListener(_showHideCursor);
914 }
915 }
916
917 void _showHideCursor() {
918 _caretPainter.shouldPaint = showCursor.value;
919 }
920
921 /// Whether the editable is currently focused.
922 bool get hasFocus => _hasFocus;
923 bool _hasFocus = false;
924 set hasFocus(bool value) {
925 if (_hasFocus == value) {
926 return;
927 }
928 _hasFocus = value;
929 markNeedsSemanticsUpdate();
930 }
931
932 /// Whether this rendering object will take a full line regardless the text width.
933 bool get forceLine => _forceLine;
934 bool _forceLine = false;
935 set forceLine(bool value) {
936 if (_forceLine == value) {
937 return;
938 }
939 _forceLine = value;
940 markNeedsLayout();
941 }
942
943 /// Whether this rendering object is read only.
944 bool get readOnly => _readOnly;
945 bool _readOnly = false;
946 set readOnly(bool value) {
947 if (_readOnly == value) {
948 return;
949 }
950 _readOnly = value;
951 markNeedsSemanticsUpdate();
952 }
953
954 /// The maximum number of lines for the text to span, wrapping if necessary.
955 ///
956 /// If this is 1 (the default), the text will not wrap, but will extend
957 /// indefinitely instead.
958 ///
959 /// If this is null, there is no limit to the number of lines.
960 ///
961 /// When this is not null, the intrinsic height of the render object is the
962 /// height of one line of text multiplied by this value. In other words, this
963 /// also controls the height of the actual editing widget.
964 int? get maxLines => _maxLines;
965 int? _maxLines;
966 /// The value may be null. If it is not null, then it must be greater than zero.
967 set maxLines(int? value) {
968 assert(value == null || value > 0);
969 if (maxLines == value) {
970 return;
971 }
972 _maxLines = value;
973
974 // Special case maxLines == 1 to keep only the first line so we can get the
975 // height of the first line in case there are hard line breaks in the text.
976 // See the `_preferredHeight` method.
977 _textPainter.maxLines = value == 1 ? 1 : null;
978 markNeedsTextLayout();
979 }
980
981 /// {@macro flutter.widgets.editableText.minLines}
982 int? get minLines => _minLines;
983 int? _minLines;
984 /// The value may be null. If it is not null, then it must be greater than zero.
985 set minLines(int? value) {
986 assert(value == null || value > 0);
987 if (minLines == value) {
988 return;
989 }
990 _minLines = value;
991 markNeedsTextLayout();
992 }
993
994 /// {@macro flutter.widgets.editableText.expands}
995 bool get expands => _expands;
996 bool _expands;
997 set expands(bool value) {
998 if (expands == value) {
999 return;
1000 }
1001 _expands = value;
1002 markNeedsTextLayout();
1003 }
1004
1005 /// The color to use when painting the selection.
1006 Color? get selectionColor => _selectionPainter.highlightColor;
1007 set selectionColor(Color? value) {
1008 _selectionPainter.highlightColor = value;
1009 }
1010
1011 /// Deprecated. Will be removed in a future version of Flutter. Use
1012 /// [textScaler] instead.
1013 ///
1014 /// The number of font pixels for each logical pixel.
1015 ///
1016 /// For example, if the text scale factor is 1.5, text will be 50% larger than
1017 /// the specified font size.
1018 @Deprecated(
1019 'Use textScaler instead. '
1020 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
1021 'This feature was deprecated after v3.12.0-2.0.pre.',
1022 )
1023 double get textScaleFactor => _textPainter.textScaleFactor;
1024 @Deprecated(
1025 'Use textScaler instead. '
1026 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
1027 'This feature was deprecated after v3.12.0-2.0.pre.',
1028 )
1029 set textScaleFactor(double value) {
1030 textScaler = TextScaler.linear(value);
1031 }
1032
1033 /// {@macro flutter.painting.textPainter.textScaler}
1034 TextScaler get textScaler => _textPainter.textScaler;
1035 set textScaler(TextScaler value) {
1036 if (_textPainter.textScaler == value) {
1037 return;
1038 }
1039 _textPainter.textScaler = value;
1040 markNeedsTextLayout();
1041 }
1042
1043 /// The region of text that is selected, if any.
1044 ///
1045 /// The caret position is represented by a collapsed selection.
1046 ///
1047 /// If [selection] is null, there is no selection and attempts to
1048 /// manipulate the selection will throw.
1049 TextSelection? get selection => _selection;
1050 TextSelection? _selection;
1051 set selection(TextSelection? value) {
1052 if (_selection == value) {
1053 return;
1054 }
1055 _selection = value;
1056 _selectionPainter.highlightedRange = value;
1057 markNeedsPaint();
1058 markNeedsSemanticsUpdate();
1059 }
1060
1061 /// The offset at which the text should be painted.
1062 ///
1063 /// If the text content is larger than the editable line itself, the editable
1064 /// line clips the text. This property controls which part of the text is
1065 /// visible by shifting the text by the given offset before clipping.
1066 ViewportOffset get offset => _offset;
1067 ViewportOffset _offset;
1068 set offset(ViewportOffset value) {
1069 if (_offset == value) {
1070 return;
1071 }
1072 if (attached) {
1073 _offset.removeListener(markNeedsPaint);
1074 }
1075 _offset = value;
1076 if (attached) {
1077 _offset.addListener(markNeedsPaint);
1078 }
1079 markNeedsLayout();
1080 }
1081
1082 /// How thick the cursor will be.
1083 double get cursorWidth => _cursorWidth;
1084 double _cursorWidth = 1.0;
1085 set cursorWidth(double value) {
1086 if (_cursorWidth == value) {
1087 return;
1088 }
1089 _cursorWidth = value;
1090 markNeedsLayout();
1091 }
1092
1093 /// How tall the cursor will be.
1094 ///
1095 /// This can be null, in which case the getter will actually return [preferredLineHeight].
1096 ///
1097 /// Setting this to itself fixes the value to the current [preferredLineHeight]. Setting
1098 /// this to null returns the behavior of deferring to [preferredLineHeight].
1099 // TODO(ianh): This is a confusing API. We should have a separate getter for the effective cursor height.
1100 double get cursorHeight => _cursorHeight ?? preferredLineHeight;
1101 double? _cursorHeight;
1102 set cursorHeight(double? value) {
1103 if (_cursorHeight == value) {
1104 return;
1105 }
1106 _cursorHeight = value;
1107 markNeedsLayout();
1108 }
1109
1110 /// {@template flutter.rendering.RenderEditable.paintCursorAboveText}
1111 /// If the cursor should be painted on top of the text or underneath it.
1112 ///
1113 /// By default, the cursor should be painted on top for iOS platforms and
1114 /// underneath for Android platforms.
1115 /// {@endtemplate}
1116 bool get paintCursorAboveText => _paintCursorOnTop;
1117 bool _paintCursorOnTop;
1118 set paintCursorAboveText(bool value) {
1119 if (_paintCursorOnTop == value) {
1120 return;
1121 }
1122 _paintCursorOnTop = value;
1123 // Clear cached built-in painters and reconfigure painters.
1124 _cachedBuiltInForegroundPainters = null;
1125 _cachedBuiltInPainters = null;
1126 // Call update methods to rebuild and set the effective painters.
1127 _updateForegroundPainter(_foregroundPainter);
1128 _updatePainter(_painter);
1129 }
1130
1131 /// {@template flutter.rendering.RenderEditable.cursorOffset}
1132 /// The offset that is used, in pixels, when painting the cursor on screen.
1133 ///
1134 /// By default, the cursor position should be set to an offset of
1135 /// (-[cursorWidth] * 0.5, 0.0) on iOS platforms and (0, 0) on Android
1136 /// platforms. The origin from where the offset is applied to is the arbitrary
1137 /// location where the cursor ends up being rendered from by default.
1138 /// {@endtemplate}
1139 Offset get cursorOffset => _caretPainter.cursorOffset;
1140 set cursorOffset(Offset value) {
1141 _caretPainter.cursorOffset = value;
1142 }
1143
1144 /// How rounded the corners of the cursor should be.
1145 ///
1146 /// A null value is the same as [Radius.zero].
1147 Radius? get cursorRadius => _caretPainter.cursorRadius;
1148 set cursorRadius(Radius? value) {
1149 _caretPainter.cursorRadius = value;
1150 }
1151
1152 /// The [LayerLink] of start selection handle.
1153 ///
1154 /// [RenderEditable] is responsible for calculating the [Offset] of this
1155 /// [LayerLink], which will be used as [CompositedTransformTarget] of start handle.
1156 LayerLink get startHandleLayerLink => _startHandleLayerLink;
1157 LayerLink _startHandleLayerLink;
1158 set startHandleLayerLink(LayerLink value) {
1159 if (_startHandleLayerLink == value) {
1160 return;
1161 }
1162 _startHandleLayerLink = value;
1163 markNeedsPaint();
1164 }
1165
1166 /// The [LayerLink] of end selection handle.
1167 ///
1168 /// [RenderEditable] is responsible for calculating the [Offset] of this
1169 /// [LayerLink], which will be used as [CompositedTransformTarget] of end handle.
1170 LayerLink get endHandleLayerLink => _endHandleLayerLink;
1171 LayerLink _endHandleLayerLink;
1172 set endHandleLayerLink(LayerLink value) {
1173 if (_endHandleLayerLink == value) {
1174 return;
1175 }
1176 _endHandleLayerLink = value;
1177 markNeedsPaint();
1178 }
1179
1180 /// The padding applied to text field. Used to determine the bounds when
1181 /// moving the floating cursor.
1182 ///
1183 /// Defaults to a padding with left, top and right set to 4, bottom to 5.
1184 EdgeInsets floatingCursorAddedMargin;
1185
1186 bool _floatingCursorOn = false;
1187 late TextPosition _floatingCursorTextPosition;
1188
1189 /// Whether to allow the user to change the selection.
1190 ///
1191 /// Since [RenderEditable] does not handle selection manipulation
1192 /// itself, this actually only affects whether the accessibility
1193 /// hints provided to the system (via
1194 /// [describeSemanticsConfiguration]) will enable selection
1195 /// manipulation. It's the responsibility of this object's owner
1196 /// to provide selection manipulation affordances.
1197 ///
1198 /// This field is used by [selectionEnabled] (which then controls
1199 /// the accessibility hints mentioned above). When null,
1200 /// [obscureText] is used to determine the value of
1201 /// [selectionEnabled] instead.
1202 bool? get enableInteractiveSelection => _enableInteractiveSelection;
1203 bool? _enableInteractiveSelection;
1204 set enableInteractiveSelection(bool? value) {
1205 if (_enableInteractiveSelection == value) {
1206 return;
1207 }
1208 _enableInteractiveSelection = value;
1209 markNeedsTextLayout();
1210 markNeedsSemanticsUpdate();
1211 }
1212
1213 /// Whether interactive selection are enabled based on the values of
1214 /// [enableInteractiveSelection] and [obscureText].
1215 ///
1216 /// Since [RenderEditable] does not handle selection manipulation
1217 /// itself, this actually only affects whether the accessibility
1218 /// hints provided to the system (via
1219 /// [describeSemanticsConfiguration]) will enable selection
1220 /// manipulation. It's the responsibility of this object's owner
1221 /// to provide selection manipulation affordances.
1222 ///
1223 /// By default, [enableInteractiveSelection] is null, [obscureText] is false,
1224 /// and this getter returns true.
1225 ///
1226 /// If [enableInteractiveSelection] is null and [obscureText] is true, then this
1227 /// getter returns false. This is the common case for password fields.
1228 ///
1229 /// If [enableInteractiveSelection] is non-null then its value is
1230 /// returned. An application might [enableInteractiveSelection] to
1231 /// true to enable interactive selection for a password field, or to
1232 /// false to unconditionally disable interactive selection.
1233 bool get selectionEnabled {
1234 return enableInteractiveSelection ?? !obscureText;
1235 }
1236
1237 /// The color used to paint the prompt rectangle.
1238 ///
1239 /// The prompt rectangle will only be requested on non-web iOS applications.
1240 // TODO(ianh): We should change the getter to return null when _promptRectRange is null
1241 // (otherwise, if you set it to null and then get it, you get back non-null).
1242 // Alternatively, we could stop supporting setting this to null.
1243 Color? get promptRectColor => _autocorrectHighlightPainter.highlightColor;
1244 set promptRectColor(Color? newValue) {
1245 _autocorrectHighlightPainter.highlightColor = newValue;
1246 }
1247
1248 /// Dismisses the currently displayed prompt rectangle and displays a new prompt rectangle
1249 /// over [newRange] in the given color [promptRectColor].
1250 ///
1251 /// The prompt rectangle will only be requested on non-web iOS applications.
1252 ///
1253 /// When set to null, the currently displayed prompt rectangle (if any) will be dismissed.
1254 // ignore: use_setters_to_change_properties, (API predates enforcing the lint)
1255 void setPromptRectRange(TextRange? newRange) {
1256 _autocorrectHighlightPainter.highlightedRange = newRange;
1257 }
1258
1259 /// The maximum amount the text is allowed to scroll.
1260 ///
1261 /// This value is only valid after layout and can change as additional
1262 /// text is entered or removed in order to accommodate expanding when
1263 /// [expands] is set to true.
1264 double get maxScrollExtent => _maxScrollExtent;
1265 double _maxScrollExtent = 0;
1266
1267 double get _caretMargin => _kCaretGap + cursorWidth;
1268
1269 /// {@macro flutter.material.Material.clipBehavior}
1270 ///
1271 /// Defaults to [Clip.hardEdge].
1272 Clip get clipBehavior => _clipBehavior;
1273 Clip _clipBehavior = Clip.hardEdge;
1274 set clipBehavior(Clip value) {
1275 if (value != _clipBehavior) {
1276 _clipBehavior = value;
1277 markNeedsPaint();
1278 markNeedsSemanticsUpdate();
1279 }
1280 }
1281
1282 /// Collected during [describeSemanticsConfiguration], used by
1283 /// [assembleSemanticsNode] and [_combineSemanticsInfo].
1284 List<InlineSpanSemanticsInformation>? _semanticsInfo;
1285
1286 // Caches [SemanticsNode]s created during [assembleSemanticsNode] so they
1287 // can be re-used when [assembleSemanticsNode] is called again. This ensures
1288 // stable ids for the [SemanticsNode]s of [TextSpan]s across
1289 // [assembleSemanticsNode] invocations.
1290 LinkedHashMap<Key, SemanticsNode>? _cachedChildNodes;
1291
1292 /// Returns a list of rects that bound the given selection, and the text
1293 /// direction. The text direction is used by the engine to calculate
1294 /// the closest position to a given point.
1295 ///
1296 /// See [TextPainter.getBoxesForSelection] for more details.
1297 List<TextBox> getBoxesForSelection(TextSelection selection) {
1298 _computeTextMetricsIfNeeded();
1299 return _textPainter.getBoxesForSelection(selection)
1300 .map((TextBox textBox) => TextBox.fromLTRBD(
1301 textBox.left + _paintOffset.dx,
1302 textBox.top + _paintOffset.dy,
1303 textBox.right + _paintOffset.dx,
1304 textBox.bottom + _paintOffset.dy,
1305 textBox.direction
1306 )).toList();
1307 }
1308
1309 @override
1310 void describeSemanticsConfiguration(SemanticsConfiguration config) {
1311 super.describeSemanticsConfiguration(config);
1312 _semanticsInfo = _textPainter.text!.getSemanticsInformation();
1313 // TODO(chunhtai): the macOS does not provide a public API to support text
1314 // selections across multiple semantics nodes. Remove this platform check
1315 // once we can support it.
1316 // https://github.com/flutter/flutter/issues/77957
1317 if (_semanticsInfo!.any((InlineSpanSemanticsInformation info) => info.recognizer != null) &&
1318 defaultTargetPlatform != TargetPlatform.macOS) {
1319 assert(readOnly && !obscureText);
1320 // For Selectable rich text with recognizer, we need to create a semantics
1321 // node for each text fragment.
1322 config
1323 ..isSemanticBoundary = true
1324 ..explicitChildNodes = true;
1325 return;
1326 }
1327 if (_cachedAttributedValue == null) {
1328 if (obscureText) {
1329 _cachedAttributedValue = AttributedString(obscuringCharacter * plainText.length);
1330 } else {
1331 final StringBuffer buffer = StringBuffer();
1332 int offset = 0;
1333 final List<StringAttribute> attributes = <StringAttribute>[];
1334 for (final InlineSpanSemanticsInformation info in _semanticsInfo!) {
1335 final String label = info.semanticsLabel ?? info.text;
1336 for (final StringAttribute infoAttribute in info.stringAttributes) {
1337 final TextRange originalRange = infoAttribute.range;
1338 attributes.add(
1339 infoAttribute.copy(
1340 range: TextRange(start: offset + originalRange.start, end: offset + originalRange.end),
1341 ),
1342 );
1343 }
1344 buffer.write(label);
1345 offset += label.length;
1346 }
1347 _cachedAttributedValue = AttributedString(buffer.toString(), attributes: attributes);
1348 }
1349 }
1350 config
1351 ..attributedValue = _cachedAttributedValue!
1352 ..isObscured = obscureText
1353 ..isMultiline = _isMultiline
1354 ..textDirection = textDirection
1355 ..isFocused = hasFocus
1356 ..isTextField = true
1357 ..isReadOnly = readOnly;
1358
1359 if (hasFocus && selectionEnabled) {
1360 config.onSetSelection = _handleSetSelection;
1361 }
1362
1363 if (hasFocus && !readOnly) {
1364 config.onSetText = _handleSetText;
1365 }
1366
1367 if (selectionEnabled && (selection?.isValid ?? false)) {
1368 config.textSelection = selection;
1369 if (_textPainter.getOffsetBefore(selection!.extentOffset) != null) {
1370 config
1371 ..onMoveCursorBackwardByWord = _handleMoveCursorBackwardByWord
1372 ..onMoveCursorBackwardByCharacter = _handleMoveCursorBackwardByCharacter;
1373 }
1374 if (_textPainter.getOffsetAfter(selection!.extentOffset) != null) {
1375 config
1376 ..onMoveCursorForwardByWord = _handleMoveCursorForwardByWord
1377 ..onMoveCursorForwardByCharacter = _handleMoveCursorForwardByCharacter;
1378 }
1379 }
1380 }
1381
1382 void _handleSetText(String text) {
1383 textSelectionDelegate.userUpdateTextEditingValue(
1384 TextEditingValue(
1385 text: text,
1386 selection: TextSelection.collapsed(offset: text.length),
1387 ),
1388 SelectionChangedCause.keyboard,
1389 );
1390 }
1391
1392 @override
1393 void assembleSemanticsNode(SemanticsNode node, SemanticsConfiguration config, Iterable<SemanticsNode> children) {
1394 assert(_semanticsInfo != null && _semanticsInfo!.isNotEmpty);
1395 final List<SemanticsNode> newChildren = <SemanticsNode>[];
1396 TextDirection currentDirection = textDirection;
1397 Rect currentRect;
1398 double ordinal = 0.0;
1399 int start = 0;
1400 int placeholderIndex = 0;
1401 int childIndex = 0;
1402 RenderBox? child = firstChild;
1403 final LinkedHashMap<Key, SemanticsNode> newChildCache = LinkedHashMap<Key, SemanticsNode>();
1404 _cachedCombinedSemanticsInfos ??= combineSemanticsInfo(_semanticsInfo!);
1405 for (final InlineSpanSemanticsInformation info in _cachedCombinedSemanticsInfos!) {
1406 final TextSelection selection = TextSelection(
1407 baseOffset: start,
1408 extentOffset: start + info.text.length,
1409 );
1410 start += info.text.length;
1411
1412 if (info.isPlaceholder) {
1413 // A placeholder span may have 0 to multiple semantics nodes, we need
1414 // to annotate all of the semantics nodes belong to this span.
1415 while (children.length > childIndex &&
1416 children.elementAt(childIndex).isTagged(PlaceholderSpanIndexSemanticsTag(placeholderIndex))) {
1417 final SemanticsNode childNode = children.elementAt(childIndex);
1418 final TextParentData parentData = child!.parentData! as TextParentData;
1419 assert(parentData.offset != null);
1420 newChildren.add(childNode);
1421 childIndex += 1;
1422 }
1423 child = childAfter(child!);
1424 placeholderIndex += 1;
1425 } else {
1426 final TextDirection initialDirection = currentDirection;
1427 final List<ui.TextBox> rects = _textPainter.getBoxesForSelection(selection);
1428 if (rects.isEmpty) {
1429 continue;
1430 }
1431 Rect rect = rects.first.toRect();
1432 currentDirection = rects.first.direction;
1433 for (final ui.TextBox textBox in rects.skip(1)) {
1434 rect = rect.expandToInclude(textBox.toRect());
1435 currentDirection = textBox.direction;
1436 }
1437 // Any of the text boxes may have had infinite dimensions.
1438 // We shouldn't pass infinite dimensions up to the bridges.
1439 rect = Rect.fromLTWH(
1440 math.max(0.0, rect.left),
1441 math.max(0.0, rect.top),
1442 math.min(rect.width, constraints.maxWidth),
1443 math.min(rect.height, constraints.maxHeight),
1444 );
1445 // Round the current rectangle to make this API testable and add some
1446 // padding so that the accessibility rects do not overlap with the text.
1447 currentRect = Rect.fromLTRB(
1448 rect.left.floorToDouble() - 4.0,
1449 rect.top.floorToDouble() - 4.0,
1450 rect.right.ceilToDouble() + 4.0,
1451 rect.bottom.ceilToDouble() + 4.0,
1452 );
1453 final SemanticsConfiguration configuration = SemanticsConfiguration()
1454 ..sortKey = OrdinalSortKey(ordinal++)
1455 ..textDirection = initialDirection
1456 ..attributedLabel = AttributedString(info.semanticsLabel ?? info.text, attributes: info.stringAttributes);
1457 final GestureRecognizer? recognizer = info.recognizer;
1458 if (recognizer != null) {
1459 if (recognizer is TapGestureRecognizer) {
1460 if (recognizer.onTap != null) {
1461 configuration.onTap = recognizer.onTap;
1462 configuration.isLink = true;
1463 }
1464 } else if (recognizer is DoubleTapGestureRecognizer) {
1465 if (recognizer.onDoubleTap != null) {
1466 configuration.onTap = recognizer.onDoubleTap;
1467 configuration.isLink = true;
1468 }
1469 } else if (recognizer is LongPressGestureRecognizer) {
1470 if (recognizer.onLongPress != null) {
1471 configuration.onLongPress = recognizer.onLongPress;
1472 }
1473 } else {
1474 assert(false, '${recognizer.runtimeType} is not supported.');
1475 }
1476 }
1477 if (node.parentPaintClipRect != null) {
1478 final Rect paintRect = node.parentPaintClipRect!.intersect(currentRect);
1479 configuration.isHidden = paintRect.isEmpty && !currentRect.isEmpty;
1480 }
1481 late final SemanticsNode newChild;
1482 if (_cachedChildNodes?.isNotEmpty ?? false) {
1483 newChild = _cachedChildNodes!.remove(_cachedChildNodes!.keys.first)!;
1484 } else {
1485 final UniqueKey key = UniqueKey();
1486 newChild = SemanticsNode(
1487 key: key,
1488 showOnScreen: _createShowOnScreenFor(key),
1489 );
1490 }
1491 newChild
1492 ..updateWith(config: configuration)
1493 ..rect = currentRect;
1494 newChildCache[newChild.key!] = newChild;
1495 newChildren.add(newChild);
1496 }
1497 }
1498 _cachedChildNodes = newChildCache;
1499 node.updateWith(config: config, childrenInInversePaintOrder: newChildren);
1500 }
1501
1502 VoidCallback? _createShowOnScreenFor(Key key) {
1503 return () {
1504 final SemanticsNode node = _cachedChildNodes![key]!;
1505 showOnScreen(descendant: this, rect: node.rect);
1506 };
1507 }
1508
1509 // TODO(ianh): in theory, [selection] could become null between when
1510 // we last called describeSemanticsConfiguration and when the
1511 // callbacks are invoked, in which case the callbacks will crash...
1512
1513 void _handleSetSelection(TextSelection selection) {
1514 _setSelection(selection, SelectionChangedCause.keyboard);
1515 }
1516
1517 void _handleMoveCursorForwardByCharacter(bool extendSelection) {
1518 assert(selection != null);
1519 final int? extentOffset = _textPainter.getOffsetAfter(selection!.extentOffset);
1520 if (extentOffset == null) {
1521 return;
1522 }
1523 final int baseOffset = !extendSelection ? extentOffset : selection!.baseOffset;
1524 _setSelection(
1525 TextSelection(baseOffset: baseOffset, extentOffset: extentOffset),
1526 SelectionChangedCause.keyboard,
1527 );
1528 }
1529
1530 void _handleMoveCursorBackwardByCharacter(bool extendSelection) {
1531 assert(selection != null);
1532 final int? extentOffset = _textPainter.getOffsetBefore(selection!.extentOffset);
1533 if (extentOffset == null) {
1534 return;
1535 }
1536 final int baseOffset = !extendSelection ? extentOffset : selection!.baseOffset;
1537 _setSelection(
1538 TextSelection(baseOffset: baseOffset, extentOffset: extentOffset),
1539 SelectionChangedCause.keyboard,
1540 );
1541 }
1542
1543 void _handleMoveCursorForwardByWord(bool extendSelection) {
1544 assert(selection != null);
1545 final TextRange currentWord = _textPainter.getWordBoundary(selection!.extent);
1546 final TextRange? nextWord = _getNextWord(currentWord.end);
1547 if (nextWord == null) {
1548 return;
1549 }
1550 final int baseOffset = extendSelection ? selection!.baseOffset : nextWord.start;
1551 _setSelection(
1552 TextSelection(
1553 baseOffset: baseOffset,
1554 extentOffset: nextWord.start,
1555 ),
1556 SelectionChangedCause.keyboard,
1557 );
1558 }
1559
1560 void _handleMoveCursorBackwardByWord(bool extendSelection) {
1561 assert(selection != null);
1562 final TextRange currentWord = _textPainter.getWordBoundary(selection!.extent);
1563 final TextRange? previousWord = _getPreviousWord(currentWord.start - 1);
1564 if (previousWord == null) {
1565 return;
1566 }
1567 final int baseOffset = extendSelection ? selection!.baseOffset : previousWord.start;
1568 _setSelection(
1569 TextSelection(
1570 baseOffset: baseOffset,
1571 extentOffset: previousWord.start,
1572 ),
1573 SelectionChangedCause.keyboard,
1574 );
1575 }
1576
1577 TextRange? _getNextWord(int offset) {
1578 while (true) {
1579 final TextRange range = _textPainter.getWordBoundary(TextPosition(offset: offset));
1580 if (!range.isValid || range.isCollapsed) {
1581 return null;
1582 }
1583 if (!_onlyWhitespace(range)) {
1584 return range;
1585 }
1586 offset = range.end;
1587 }
1588 }
1589
1590 TextRange? _getPreviousWord(int offset) {
1591 while (offset >= 0) {
1592 final TextRange range = _textPainter.getWordBoundary(TextPosition(offset: offset));
1593 if (!range.isValid || range.isCollapsed) {
1594 return null;
1595 }
1596 if (!_onlyWhitespace(range)) {
1597 return range;
1598 }
1599 offset = range.start - 1;
1600 }
1601 return null;
1602 }
1603
1604 // Check if the given text range only contains white space or separator
1605 // characters.
1606 //
1607 // Includes newline characters from ASCII and separators from the
1608 // [unicode separator category](https://www.compart.com/en/unicode/category/Zs)
1609 // TODO(zanderso): replace when we expose this ICU information.
1610 bool _onlyWhitespace(TextRange range) {
1611 for (int i = range.start; i < range.end; i++) {
1612 final int codeUnit = text!.codeUnitAt(i)!;
1613 if (!TextLayoutMetrics.isWhitespace(codeUnit)) {
1614 return false;
1615 }
1616 }
1617 return true;
1618 }
1619
1620 @override
1621 void attach(PipelineOwner owner) {
1622 super.attach(owner);
1623 _foregroundRenderObject?.attach(owner);
1624 _backgroundRenderObject?.attach(owner);
1625
1626 _tap = TapGestureRecognizer(debugOwner: this)
1627 ..onTapDown = _handleTapDown
1628 ..onTap = _handleTap;
1629 _longPress = LongPressGestureRecognizer(debugOwner: this)..onLongPress = _handleLongPress;
1630 _offset.addListener(markNeedsPaint);
1631 _showHideCursor();
1632 _showCursor.addListener(_showHideCursor);
1633 }
1634
1635 @override
1636 void detach() {
1637 _tap.dispose();
1638 _longPress.dispose();
1639 _offset.removeListener(markNeedsPaint);
1640 _showCursor.removeListener(_showHideCursor);
1641 super.detach();
1642 _foregroundRenderObject?.detach();
1643 _backgroundRenderObject?.detach();
1644 }
1645
1646 @override
1647 void redepthChildren() {
1648 final RenderObject? foregroundChild = _foregroundRenderObject;
1649 final RenderObject? backgroundChild = _backgroundRenderObject;
1650 if (foregroundChild != null) {
1651 redepthChild(foregroundChild);
1652 }
1653 if (backgroundChild != null) {
1654 redepthChild(backgroundChild);
1655 }
1656 super.redepthChildren();
1657 }
1658
1659 @override
1660 void visitChildren(RenderObjectVisitor visitor) {
1661 final RenderObject? foregroundChild = _foregroundRenderObject;
1662 final RenderObject? backgroundChild = _backgroundRenderObject;
1663 if (foregroundChild != null) {
1664 visitor(foregroundChild);
1665 }
1666 if (backgroundChild != null) {
1667 visitor(backgroundChild);
1668 }
1669 super.visitChildren(visitor);
1670 }
1671
1672 bool get _isMultiline => maxLines != 1;
1673
1674 Axis get _viewportAxis => _isMultiline ? Axis.vertical : Axis.horizontal;
1675
1676 Offset get _paintOffset {
1677 switch (_viewportAxis) {
1678 case Axis.horizontal:
1679 return Offset(-offset.pixels, 0.0);
1680 case Axis.vertical:
1681 return Offset(0.0, -offset.pixels);
1682 }
1683 }
1684
1685 double get _viewportExtent {
1686 assert(hasSize);
1687 switch (_viewportAxis) {
1688 case Axis.horizontal:
1689 return size.width;
1690 case Axis.vertical:
1691 return size.height;
1692 }
1693 }
1694
1695 double _getMaxScrollExtent(Size contentSize) {
1696 assert(hasSize);
1697 switch (_viewportAxis) {
1698 case Axis.horizontal:
1699 return math.max(0.0, contentSize.width - size.width);
1700 case Axis.vertical:
1701 return math.max(0.0, contentSize.height - size.height);
1702 }
1703 }
1704
1705 // We need to check the paint offset here because during animation, the start of
1706 // the text may position outside the visible region even when the text fits.
1707 bool get _hasVisualOverflow => _maxScrollExtent > 0 || _paintOffset != Offset.zero;
1708
1709 /// Returns the local coordinates of the endpoints of the given selection.
1710 ///
1711 /// If the selection is collapsed (and therefore occupies a single point), the
1712 /// returned list is of length one. Otherwise, the selection is not collapsed
1713 /// and the returned list is of length two. In this case, however, the two
1714 /// points might actually be co-located (e.g., because of a bidirectional
1715 /// selection that contains some text but whose ends meet in the middle).
1716 ///
1717 /// See also:
1718 ///
1719 /// * [getLocalRectForCaret], which is the equivalent but for
1720 /// a [TextPosition] rather than a [TextSelection].
1721 List<TextSelectionPoint> getEndpointsForSelection(TextSelection selection) {
1722 _computeTextMetricsIfNeeded();
1723
1724 final Offset paintOffset = _paintOffset;
1725
1726 final List<ui.TextBox> boxes = selection.isCollapsed ?
1727 <ui.TextBox>[] : _textPainter.getBoxesForSelection(selection, boxHeightStyle: selectionHeightStyle, boxWidthStyle: selectionWidthStyle);
1728 if (boxes.isEmpty) {
1729 // TODO(mpcomplete): This doesn't work well at an RTL/LTR boundary.
1730 final Offset caretOffset = _textPainter.getOffsetForCaret(selection.extent, _caretPrototype);
1731 final Offset start = Offset(0.0, preferredLineHeight) + caretOffset + paintOffset;
1732 return <TextSelectionPoint>[TextSelectionPoint(start, null)];
1733 } else {
1734 final Offset start = Offset(clampDouble(boxes.first.start, 0, _textPainter.size.width), boxes.first.bottom) + paintOffset;
1735 final Offset end = Offset(clampDouble(boxes.last.end, 0, _textPainter.size.width), boxes.last.bottom) + paintOffset;
1736 return <TextSelectionPoint>[
1737 TextSelectionPoint(start, boxes.first.direction),
1738 TextSelectionPoint(end, boxes.last.direction),
1739 ];
1740 }
1741 }
1742
1743 /// Returns the smallest [Rect], in the local coordinate system, that covers
1744 /// the text within the [TextRange] specified.
1745 ///
1746 /// This method is used to calculate the approximate position of the IME bar
1747 /// on iOS.
1748 ///
1749 /// Returns null if [TextRange.isValid] is false for the given `range`, or the
1750 /// given `range` is collapsed.
1751 Rect? getRectForComposingRange(TextRange range) {
1752 if (!range.isValid || range.isCollapsed) {
1753 return null;
1754 }
1755 _computeTextMetricsIfNeeded();
1756
1757 final List<ui.TextBox> boxes = _textPainter.getBoxesForSelection(
1758 TextSelection(baseOffset: range.start, extentOffset: range.end),
1759 boxHeightStyle: selectionHeightStyle,
1760 boxWidthStyle: selectionWidthStyle,
1761 );
1762
1763 return boxes.fold(
1764 null,
1765 (Rect? accum, TextBox incoming) => accum?.expandToInclude(incoming.toRect()) ?? incoming.toRect(),
1766 )?.shift(_paintOffset);
1767 }
1768
1769 /// Returns the position in the text for the given global coordinate.
1770 ///
1771 /// See also:
1772 ///
1773 /// * [getLocalRectForCaret], which is the reverse operation, taking
1774 /// a [TextPosition] and returning a [Rect].
1775 /// * [TextPainter.getPositionForOffset], which is the equivalent method
1776 /// for a [TextPainter] object.
1777 TextPosition getPositionForPoint(Offset globalPosition) {
1778 _computeTextMetricsIfNeeded();
1779 globalPosition += -_paintOffset;
1780 return _textPainter.getPositionForOffset(globalToLocal(globalPosition));
1781 }
1782
1783 /// Returns the [Rect] in local coordinates for the caret at the given text
1784 /// position.
1785 ///
1786 /// See also:
1787 ///
1788 /// * [getPositionForPoint], which is the reverse operation, taking
1789 /// an [Offset] in global coordinates and returning a [TextPosition].
1790 /// * [getEndpointsForSelection], which is the equivalent but for
1791 /// a selection rather than a particular text position.
1792 /// * [TextPainter.getOffsetForCaret], the equivalent method for a
1793 /// [TextPainter] object.
1794 Rect getLocalRectForCaret(TextPosition caretPosition) {
1795 _computeTextMetricsIfNeeded();
1796 final Rect caretPrototype = _caretPrototype;
1797 final Offset caretOffset = _textPainter.getOffsetForCaret(caretPosition, caretPrototype);
1798 Rect caretRect = caretPrototype.shift(caretOffset + cursorOffset);
1799 final double scrollableWidth = math.max(_textPainter.width + _caretMargin, size.width);
1800
1801 final double caretX = clampDouble(caretRect.left, 0, math.max(scrollableWidth - _caretMargin, 0));
1802 caretRect = Offset(caretX, caretRect.top) & caretRect.size;
1803
1804 final double caretHeight = cursorHeight;
1805 switch (defaultTargetPlatform) {
1806 case TargetPlatform.iOS:
1807 case TargetPlatform.macOS:
1808 final double fullHeight = _textPainter.getFullHeightForCaret(caretPosition, caretPrototype) ?? _textPainter.preferredLineHeight;
1809 final double heightDiff = fullHeight - caretRect.height;
1810 // Center the caret vertically along the text.
1811 caretRect = Rect.fromLTWH(
1812 caretRect.left,
1813 caretRect.top + heightDiff / 2,
1814 caretRect.width,
1815 caretRect.height,
1816 );
1817 case TargetPlatform.android:
1818 case TargetPlatform.fuchsia:
1819 case TargetPlatform.linux:
1820 case TargetPlatform.windows:
1821 // Override the height to take the full height of the glyph at the TextPosition
1822 // when not on iOS. iOS has special handling that creates a taller caret.
1823 // TODO(garyq): See the TODO for _computeCaretPrototype().
1824 caretRect = Rect.fromLTWH(
1825 caretRect.left,
1826 caretRect.top - _kCaretHeightOffset,
1827 caretRect.width,
1828 caretHeight,
1829 );
1830 }
1831
1832 caretRect = caretRect.shift(_paintOffset);
1833 return caretRect.shift(_snapToPhysicalPixel(caretRect.topLeft));
1834 }
1835
1836 @override
1837 double computeMinIntrinsicWidth(double height) {
1838 if (!_canComputeIntrinsics) {
1839 return 0.0;
1840 }
1841 _textPainter.setPlaceholderDimensions(layoutInlineChildren(
1842 double.infinity,
1843 (RenderBox child, BoxConstraints constraints) => Size(child.getMinIntrinsicWidth(double.infinity), 0.0),
1844 ));
1845 _layoutText();
1846 return _textPainter.minIntrinsicWidth;
1847 }
1848
1849 @override
1850 double computeMaxIntrinsicWidth(double height) {
1851 if (!_canComputeIntrinsics) {
1852 return 0.0;
1853 }
1854 _textPainter.setPlaceholderDimensions(layoutInlineChildren(
1855 double.infinity,
1856 // Height and baseline is irrelevant as all text will be laid
1857 // out in a single line. Therefore, using 0.0 as a dummy for the height.
1858 (RenderBox child, BoxConstraints constraints) => Size(child.getMaxIntrinsicWidth(double.infinity), 0.0),
1859 ));
1860 _layoutText();
1861 return _textPainter.maxIntrinsicWidth + _caretMargin;
1862 }
1863
1864 /// An estimate of the height of a line in the text. See [TextPainter.preferredLineHeight].
1865 /// This does not require the layout to be updated.
1866 double get preferredLineHeight => _textPainter.preferredLineHeight;
1867
1868 int? _cachedLineBreakCount;
1869 // TODO(LongCatIsLooong): see if we can let ui.Paragraph estimate the number
1870 // of lines
1871 int _countHardLineBreaks(String text) {
1872 final int? cachedValue = _cachedLineBreakCount;
1873 if (cachedValue != null) {
1874 return cachedValue;
1875 }
1876 int count = 0;
1877 for (int index = 0; index < text.length; index += 1) {
1878 switch (text.codeUnitAt(index)) {
1879 case 0x000A: // LF
1880 case 0x0085: // NEL
1881 case 0x000B: // VT
1882 case 0x000C: // FF, treating it as a regular line separator
1883 case 0x2028: // LS
1884 case 0x2029: // PS
1885 count += 1;
1886 }
1887 }
1888 return _cachedLineBreakCount = count;
1889 }
1890
1891 double _preferredHeight(double width) {
1892 final int? maxLines = this.maxLines;
1893 final int? minLines = this.minLines ?? maxLines;
1894 final double minHeight = preferredLineHeight * (minLines ?? 0);
1895
1896 if (maxLines == null) {
1897 final double estimatedHeight;
1898 if (width == double.infinity) {
1899 estimatedHeight = preferredLineHeight * (_countHardLineBreaks(plainText) + 1);
1900 } else {
1901 _layoutText(maxWidth: width);
1902 estimatedHeight = _textPainter.height;
1903 }
1904 return math.max(estimatedHeight, minHeight);
1905 }
1906
1907 // Special case maxLines == 1 since it forces the scrollable direction
1908 // to be horizontal. Report the real height to prevent the text from being
1909 // clipped.
1910 if (maxLines == 1) {
1911 // The _layoutText call lays out the paragraph using infinite width when
1912 // maxLines == 1. Also _textPainter.maxLines will be set to 1 so should
1913 // there be any line breaks only the first line is shown.
1914 assert(_textPainter.maxLines == 1);
1915 _layoutText(maxWidth: width);
1916 return _textPainter.height;
1917 }
1918 if (minLines == maxLines) {
1919 return minHeight;
1920 }
1921 _layoutText(maxWidth: width);
1922 final double maxHeight = preferredLineHeight * maxLines;
1923 return clampDouble(_textPainter.height, minHeight, maxHeight);
1924 }
1925
1926 @override
1927 double computeMinIntrinsicHeight(double width) => computeMaxIntrinsicHeight(width);
1928
1929 @override
1930 double computeMaxIntrinsicHeight(double width) {
1931 if (!_canComputeIntrinsics) {
1932 return 0.0;
1933 }
1934 _textPainter.setPlaceholderDimensions(layoutInlineChildren(width, ChildLayoutHelper.dryLayoutChild));
1935 return _preferredHeight(width);
1936 }
1937
1938 @override
1939 double computeDistanceToActualBaseline(TextBaseline baseline) {
1940 _computeTextMetricsIfNeeded();
1941 return _textPainter.computeDistanceToActualBaseline(baseline);
1942 }
1943
1944 @override
1945 bool hitTestSelf(Offset position) => true;
1946
1947 @override
1948 @protected
1949 bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
1950 final Offset effectivePosition = position - _paintOffset;
1951 final GlyphInfo? glyph = _textPainter.getClosestGlyphForOffset(effectivePosition);
1952 // The hit-test can't fall through the horizontal gaps between visually
1953 // adjacent characters on the same line, even with a large letter-spacing or
1954 // text justification, as graphemeClusterLayoutBounds.width is the advance
1955 // width to the next character, so there's no gap between their
1956 // graphemeClusterLayoutBounds rects.
1957 final InlineSpan? spanHit = glyph != null && glyph.graphemeClusterLayoutBounds.contains(effectivePosition)
1958 ? _textPainter.text!.getSpanForPosition(TextPosition(offset: glyph.graphemeClusterCodeUnitRange.start))
1959 : null;
1960 switch (spanHit) {
1961 case final HitTestTarget span:
1962 result.add(HitTestEntry(span));
1963 return true;
1964 case _:
1965 return hitTestInlineChildren(result, effectivePosition);
1966 }
1967 }
1968
1969 late TapGestureRecognizer _tap;
1970 late LongPressGestureRecognizer _longPress;
1971
1972 @override
1973 void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
1974 assert(debugHandleEvent(event, entry));
1975 if (event is PointerDownEvent) {
1976 assert(!debugNeedsLayout);
1977
1978 if (!ignorePointer) {
1979 // Propagates the pointer event to selection handlers.
1980 _tap.addPointer(event);
1981 _longPress.addPointer(event);
1982 }
1983 }
1984 }
1985
1986 Offset? _lastTapDownPosition;
1987 Offset? _lastSecondaryTapDownPosition;
1988
1989 /// {@template flutter.rendering.RenderEditable.lastSecondaryTapDownPosition}
1990 /// The position of the most recent secondary tap down event on this text
1991 /// input.
1992 /// {@endtemplate}
1993 Offset? get lastSecondaryTapDownPosition => _lastSecondaryTapDownPosition;
1994
1995 /// Tracks the position of a secondary tap event.
1996 ///
1997 /// Should be called before attempting to change the selection based on the
1998 /// position of a secondary tap.
1999 void handleSecondaryTapDown(TapDownDetails details) {
2000 _lastTapDownPosition = details.globalPosition;
2001 _lastSecondaryTapDownPosition = details.globalPosition;
2002 }
2003
2004 /// If [ignorePointer] is false (the default) then this method is called by
2005 /// the internal gesture recognizer's [TapGestureRecognizer.onTapDown]
2006 /// callback.
2007 ///
2008 /// When [ignorePointer] is true, an ancestor widget must respond to tap
2009 /// down events by calling this method.
2010 void handleTapDown(TapDownDetails details) {
2011 _lastTapDownPosition = details.globalPosition;
2012 }
2013 void _handleTapDown(TapDownDetails details) {
2014 assert(!ignorePointer);
2015 handleTapDown(details);
2016 }
2017
2018 /// If [ignorePointer] is false (the default) then this method is called by
2019 /// the internal gesture recognizer's [TapGestureRecognizer.onTap]
2020 /// callback.
2021 ///
2022 /// When [ignorePointer] is true, an ancestor widget must respond to tap
2023 /// events by calling this method.
2024 void handleTap() {
2025 selectPosition(cause: SelectionChangedCause.tap);
2026 }
2027 void _handleTap() {
2028 assert(!ignorePointer);
2029 handleTap();
2030 }
2031
2032 /// If [ignorePointer] is false (the default) then this method is called by
2033 /// the internal gesture recognizer's [DoubleTapGestureRecognizer.onDoubleTap]
2034 /// callback.
2035 ///
2036 /// When [ignorePointer] is true, an ancestor widget must respond to double
2037 /// tap events by calling this method.
2038 void handleDoubleTap() {
2039 selectWord(cause: SelectionChangedCause.doubleTap);
2040 }
2041
2042 /// If [ignorePointer] is false (the default) then this method is called by
2043 /// the internal gesture recognizer's [LongPressGestureRecognizer.onLongPress]
2044 /// callback.
2045 ///
2046 /// When [ignorePointer] is true, an ancestor widget must respond to long
2047 /// press events by calling this method.
2048 void handleLongPress() {
2049 selectWord(cause: SelectionChangedCause.longPress);
2050 }
2051 void _handleLongPress() {
2052 assert(!ignorePointer);
2053 handleLongPress();
2054 }
2055
2056 /// Move selection to the location of the last tap down.
2057 ///
2058 /// {@template flutter.rendering.RenderEditable.selectPosition}
2059 /// This method is mainly used to translate user inputs in global positions
2060 /// into a [TextSelection]. When used in conjunction with a [EditableText],
2061 /// the selection change is fed back into [TextEditingController.selection].
2062 ///
2063 /// If you have a [TextEditingController], it's generally easier to
2064 /// programmatically manipulate its `value` or `selection` directly.
2065 /// {@endtemplate}
2066 void selectPosition({ required SelectionChangedCause cause }) {
2067 selectPositionAt(from: _lastTapDownPosition!, cause: cause);
2068 }
2069
2070 /// Select text between the global positions [from] and [to].
2071 ///
2072 /// [from] corresponds to the [TextSelection.baseOffset], and [to] corresponds
2073 /// to the [TextSelection.extentOffset].
2074 void selectPositionAt({ required Offset from, Offset? to, required SelectionChangedCause cause }) {
2075 _layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
2076 final TextPosition fromPosition = _textPainter.getPositionForOffset(globalToLocal(from - _paintOffset));
2077 final TextPosition? toPosition = to == null
2078 ? null
2079 : _textPainter.getPositionForOffset(globalToLocal(to - _paintOffset));
2080
2081 final int baseOffset = fromPosition.offset;
2082 final int extentOffset = toPosition?.offset ?? fromPosition.offset;
2083
2084 final TextSelection newSelection = TextSelection(
2085 baseOffset: baseOffset,
2086 extentOffset: extentOffset,
2087 affinity: fromPosition.affinity,
2088 );
2089
2090 _setSelection(newSelection, cause);
2091 }
2092
2093 /// {@macro flutter.painting.TextPainter.wordBoundaries}
2094 WordBoundary get wordBoundaries => _textPainter.wordBoundaries;
2095
2096 /// Select a word around the location of the last tap down.
2097 ///
2098 /// {@macro flutter.rendering.RenderEditable.selectPosition}
2099 void selectWord({ required SelectionChangedCause cause }) {
2100 selectWordsInRange(from: _lastTapDownPosition!, cause: cause);
2101 }
2102
2103 /// Selects the set words of a paragraph that intersect a given range of global positions.
2104 ///
2105 /// The set of words selected are not strictly bounded by the range of global positions.
2106 ///
2107 /// The first and last endpoints of the selection will always be at the
2108 /// beginning and end of a word respectively.
2109 ///
2110 /// {@macro flutter.rendering.RenderEditable.selectPosition}
2111 void selectWordsInRange({ required Offset from, Offset? to, required SelectionChangedCause cause }) {
2112 _computeTextMetricsIfNeeded();
2113 final TextPosition fromPosition = _textPainter.getPositionForOffset(globalToLocal(from - _paintOffset));
2114 final TextSelection fromWord = getWordAtOffset(fromPosition);
2115 final TextPosition toPosition = to == null ? fromPosition : _textPainter.getPositionForOffset(globalToLocal(to - _paintOffset));
2116 final TextSelection toWord = toPosition == fromPosition ? fromWord : getWordAtOffset(toPosition);
2117 final bool isFromWordBeforeToWord = fromWord.start < toWord.end;
2118
2119 _setSelection(
2120 TextSelection(
2121 baseOffset: isFromWordBeforeToWord ? fromWord.base.offset : fromWord.extent.offset,
2122 extentOffset: isFromWordBeforeToWord ? toWord.extent.offset : toWord.base.offset,
2123 affinity: fromWord.affinity,
2124 ),
2125 cause,
2126 );
2127 }
2128
2129 /// Move the selection to the beginning or end of a word.
2130 ///
2131 /// {@macro flutter.rendering.RenderEditable.selectPosition}
2132 void selectWordEdge({ required SelectionChangedCause cause }) {
2133 _computeTextMetricsIfNeeded();
2134 assert(_lastTapDownPosition != null);
2135 final TextPosition position = _textPainter.getPositionForOffset(globalToLocal(_lastTapDownPosition! - _paintOffset));
2136 final TextRange word = _textPainter.getWordBoundary(position);
2137 late TextSelection newSelection;
2138 if (position.offset <= word.start) {
2139 newSelection = TextSelection.collapsed(offset: word.start);
2140 } else {
2141 newSelection = TextSelection.collapsed(offset: word.end, affinity: TextAffinity.upstream);
2142 }
2143 _setSelection(newSelection, cause);
2144 }
2145
2146 /// Returns a [TextSelection] that encompasses the word at the given
2147 /// [TextPosition].
2148 @visibleForTesting
2149 TextSelection getWordAtOffset(TextPosition position) {
2150 debugAssertLayoutUpToDate();
2151 // When long-pressing past the end of the text, we want a collapsed cursor.
2152 if (position.offset >= plainText.length) {
2153 return TextSelection.fromPosition(
2154 TextPosition(offset: plainText.length, affinity: TextAffinity.upstream)
2155 );
2156 }
2157 // If text is obscured, the entire sentence should be treated as one word.
2158 if (obscureText) {
2159 return TextSelection(baseOffset: 0, extentOffset: plainText.length);
2160 }
2161 final TextRange word = _textPainter.getWordBoundary(position);
2162 final int effectiveOffset;
2163 switch (position.affinity) {
2164 case TextAffinity.upstream:
2165 // upstream affinity is effectively -1 in text position.
2166 effectiveOffset = position.offset - 1;
2167 case TextAffinity.downstream:
2168 effectiveOffset = position.offset;
2169 }
2170 assert(effectiveOffset >= 0);
2171
2172 // On iOS, select the previous word if there is a previous word, or select
2173 // to the end of the next word if there is a next word. Select nothing if
2174 // there is neither a previous word nor a next word.
2175 //
2176 // If the platform is Android and the text is read only, try to select the
2177 // previous word if there is one; otherwise, select the single whitespace at
2178 // the position.
2179 if (effectiveOffset > 0
2180 && TextLayoutMetrics.isWhitespace(plainText.codeUnitAt(effectiveOffset))) {
2181 final TextRange? previousWord = _getPreviousWord(word.start);
2182 switch (defaultTargetPlatform) {
2183 case TargetPlatform.iOS:
2184 if (previousWord == null) {
2185 final TextRange? nextWord = _getNextWord(word.start);
2186 if (nextWord == null) {
2187 return TextSelection.collapsed(offset: position.offset);
2188 }
2189 return TextSelection(
2190 baseOffset: position.offset,
2191 extentOffset: nextWord.end,
2192 );
2193 }
2194 return TextSelection(
2195 baseOffset: previousWord.start,
2196 extentOffset: position.offset,
2197 );
2198 case TargetPlatform.android:
2199 if (readOnly) {
2200 if (previousWord == null) {
2201 return TextSelection(
2202 baseOffset: position.offset,
2203 extentOffset: position.offset + 1,
2204 );
2205 }
2206 return TextSelection(
2207 baseOffset: previousWord.start,
2208 extentOffset: position.offset,
2209 );
2210 }
2211 case TargetPlatform.fuchsia:
2212 case TargetPlatform.macOS:
2213 case TargetPlatform.linux:
2214 case TargetPlatform.windows:
2215 break;
2216 }
2217 }
2218
2219 return TextSelection(baseOffset: word.start, extentOffset: word.end);
2220 }
2221
2222 // Placeholder dimensions representing the sizes of child inline widgets.
2223 //
2224 // These need to be cached because the text painter's placeholder dimensions
2225 // will be overwritten during intrinsic width/height calculations and must be
2226 // restored to the original values before final layout and painting.
2227 List<PlaceholderDimensions>? _placeholderDimensions;
2228
2229 void _layoutText({ double minWidth = 0.0, double maxWidth = double.infinity }) {
2230 final double availableMaxWidth = math.max(0.0, maxWidth - _caretMargin);
2231 final double availableMinWidth = math.min(minWidth, availableMaxWidth);
2232 final double textMaxWidth = _isMultiline ? availableMaxWidth : double.infinity;
2233 final double textMinWidth = forceLine ? availableMaxWidth : availableMinWidth;
2234 _textPainter.layout(
2235 minWidth: textMinWidth,
2236 maxWidth: textMaxWidth,
2237 );
2238 _textLayoutLastMinWidth = minWidth;
2239 _textLayoutLastMaxWidth = maxWidth;
2240 }
2241
2242 // Computes the text metrics if `_textPainter`'s layout information was marked
2243 // as dirty.
2244 //
2245 // This method must be called in `RenderEditable`'s public methods that expose
2246 // `_textPainter`'s metrics. For instance, `systemFontsDidChange` sets
2247 // _textPainter._paragraph to null, so accessing _textPainter's metrics
2248 // immediately after `systemFontsDidChange` without first calling this method
2249 // may crash.
2250 //
2251 // This method is also called in various paint methods (`RenderEditable.paint`
2252 // as well as its foreground/background painters' `paint`). It's needed
2253 // because invisible render objects kept in the tree by `KeepAlive` may not
2254 // get a chance to do layout but can still paint.
2255 // See https://github.com/flutter/flutter/issues/84896.
2256 //
2257 // This method only re-computes layout if the underlying `_textPainter`'s
2258 // layout cache is invalidated (by calling `TextPainter.markNeedsLayout`), or
2259 // the constraints used to layout the `_textPainter` is different. See
2260 // `TextPainter.layout`.
2261 void _computeTextMetricsIfNeeded() {
2262 _layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
2263 }
2264
2265 late Rect _caretPrototype;
2266
2267 // TODO(LongCatIsLooong): https://github.com/flutter/flutter/issues/120836
2268 //
2269 /// On iOS, the cursor is taller than the cursor on Android. The height
2270 /// of the cursor for iOS is approximate and obtained through an eyeball
2271 /// comparison.
2272 void _computeCaretPrototype() {
2273 switch (defaultTargetPlatform) {
2274 case TargetPlatform.iOS:
2275 case TargetPlatform.macOS:
2276 _caretPrototype = Rect.fromLTWH(0.0, 0.0, cursorWidth, cursorHeight + 2);
2277 case TargetPlatform.android:
2278 case TargetPlatform.fuchsia:
2279 case TargetPlatform.linux:
2280 case TargetPlatform.windows:
2281 _caretPrototype = Rect.fromLTWH(0.0, _kCaretHeightOffset, cursorWidth, cursorHeight - 2.0 * _kCaretHeightOffset);
2282 }
2283 }
2284
2285 // Computes the offset to apply to the given [sourceOffset] so it perfectly
2286 // snaps to physical pixels.
2287 Offset _snapToPhysicalPixel(Offset sourceOffset) {
2288 final Offset globalOffset = localToGlobal(sourceOffset);
2289 final double pixelMultiple = 1.0 / _devicePixelRatio;
2290 return Offset(
2291 globalOffset.dx.isFinite
2292 ? (globalOffset.dx / pixelMultiple).round() * pixelMultiple - globalOffset.dx
2293 : 0,
2294 globalOffset.dy.isFinite
2295 ? (globalOffset.dy / pixelMultiple).round() * pixelMultiple - globalOffset.dy
2296 : 0,
2297 );
2298 }
2299
2300 bool _canComputeDryLayoutForInlineWidgets() {
2301 return text?.visitChildren((InlineSpan span) {
2302 return (span is! PlaceholderSpan) || switch (span.alignment) {
2303 ui.PlaceholderAlignment.baseline ||
2304 ui.PlaceholderAlignment.aboveBaseline ||
2305 ui.PlaceholderAlignment.belowBaseline => false,
2306 ui.PlaceholderAlignment.top ||
2307 ui.PlaceholderAlignment.middle ||
2308 ui.PlaceholderAlignment.bottom => true,
2309 };
2310 }) ?? true;
2311 }
2312
2313 bool? _canComputeIntrinsicsCached;
2314 bool get _canComputeIntrinsics => _canComputeIntrinsicsCached ??= _canComputeDryLayoutForInlineWidgets();
2315
2316 @override
2317 @protected
2318 Size computeDryLayout(covariant BoxConstraints constraints) {
2319 if (!_canComputeIntrinsics) {
2320 assert(debugCannotComputeDryLayout(
2321 reason: 'Dry layout not available for alignments that require baseline.',
2322 ));
2323 return Size.zero;
2324 }
2325 _textPainter.setPlaceholderDimensions(layoutInlineChildren(constraints.maxWidth, ChildLayoutHelper.dryLayoutChild));
2326 _layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
2327 final double width = forceLine ? constraints.maxWidth : constraints
2328 .constrainWidth(_textPainter.size.width + _caretMargin);
2329 return Size(width, constraints.constrainHeight(_preferredHeight(constraints.maxWidth)));
2330 }
2331
2332 @override
2333 void performLayout() {
2334 final BoxConstraints constraints = this.constraints;
2335 _placeholderDimensions = layoutInlineChildren(constraints.maxWidth, ChildLayoutHelper.layoutChild);
2336 _textPainter.setPlaceholderDimensions(_placeholderDimensions);
2337 _computeTextMetricsIfNeeded();
2338 positionInlineChildren(_textPainter.inlinePlaceholderBoxes!);
2339 _computeCaretPrototype();
2340 // We grab _textPainter.size here because assigning to `size` on the next
2341 // line will trigger us to validate our intrinsic sizes, which will change
2342 // _textPainter's layout because the intrinsic size calculations are
2343 // destructive, which would mean we would get different results if we later
2344 // used properties on _textPainter in this method.
2345 // Other _textPainter state like didExceedMaxLines will also be affected,
2346 // though we currently don't use those here.
2347 // See also RenderParagraph which has a similar issue.
2348 final Size textPainterSize = _textPainter.size;
2349 final double width = forceLine ? constraints.maxWidth : constraints
2350 .constrainWidth(_textPainter.size.width + _caretMargin);
2351 final double preferredHeight = _preferredHeight(constraints.maxWidth);
2352 size = Size(width, constraints.constrainHeight(preferredHeight));
2353 final Size contentSize = Size(textPainterSize.width + _caretMargin, textPainterSize.height);
2354
2355 final BoxConstraints painterConstraints = BoxConstraints.tight(contentSize);
2356
2357 _foregroundRenderObject?.layout(painterConstraints);
2358 _backgroundRenderObject?.layout(painterConstraints);
2359
2360 _maxScrollExtent = _getMaxScrollExtent(contentSize);
2361 offset.applyViewportDimension(_viewportExtent);
2362 offset.applyContentDimensions(0.0, _maxScrollExtent);
2363 }
2364
2365 // The relative origin in relation to the distance the user has theoretically
2366 // dragged the floating cursor offscreen. This value is used to account for the
2367 // difference in the rendering position and the raw offset value.
2368 Offset _relativeOrigin = Offset.zero;
2369 Offset? _previousOffset;
2370 bool _shouldResetOrigin = true;
2371 bool _resetOriginOnLeft = false;
2372 bool _resetOriginOnRight = false;
2373 bool _resetOriginOnTop = false;
2374 bool _resetOriginOnBottom = false;
2375 double? _resetFloatingCursorAnimationValue;
2376
2377 static Offset _calculateAdjustedCursorOffset(Offset offset, Rect boundingRects) {
2378 final double adjustedX = clampDouble(offset.dx, boundingRects.left, boundingRects.right);
2379 final double adjustedY = clampDouble(offset.dy, boundingRects.top, boundingRects.bottom);
2380 return Offset(adjustedX, adjustedY);
2381 }
2382
2383 /// Returns the position within the text field closest to the raw cursor offset.
2384 Offset calculateBoundedFloatingCursorOffset(Offset rawCursorOffset, {bool? shouldResetOrigin}) {
2385 Offset deltaPosition = Offset.zero;
2386 final double topBound = -floatingCursorAddedMargin.top;
2387 final double bottomBound = math.min(size.height, _textPainter.height) - preferredLineHeight + floatingCursorAddedMargin.bottom;
2388 final double leftBound = -floatingCursorAddedMargin.left;
2389 final double rightBound = math.min(size.width, _textPainter.width) + floatingCursorAddedMargin.right;
2390 final Rect boundingRects = Rect.fromLTRB(leftBound, topBound, rightBound, bottomBound);
2391
2392 if (shouldResetOrigin != null) {
2393 _shouldResetOrigin = shouldResetOrigin;
2394 }
2395
2396 if (!_shouldResetOrigin) {
2397 return _calculateAdjustedCursorOffset(rawCursorOffset, boundingRects);
2398 }
2399
2400 if (_previousOffset != null) {
2401 deltaPosition = rawCursorOffset - _previousOffset!;
2402 }
2403
2404 // If the raw cursor offset has gone off an edge, we want to reset the relative
2405 // origin of the dragging when the user drags back into the field.
2406 if (_resetOriginOnLeft && deltaPosition.dx > 0) {
2407 _relativeOrigin = Offset(rawCursorOffset.dx - boundingRects.left, _relativeOrigin.dy);
2408 _resetOriginOnLeft = false;
2409 } else if (_resetOriginOnRight && deltaPosition.dx < 0) {
2410 _relativeOrigin = Offset(rawCursorOffset.dx - boundingRects.right, _relativeOrigin.dy);
2411 _resetOriginOnRight = false;
2412 }
2413 if (_resetOriginOnTop && deltaPosition.dy > 0) {
2414 _relativeOrigin = Offset(_relativeOrigin.dx, rawCursorOffset.dy - boundingRects.top);
2415 _resetOriginOnTop = false;
2416 } else if (_resetOriginOnBottom && deltaPosition.dy < 0) {
2417 _relativeOrigin = Offset(_relativeOrigin.dx, rawCursorOffset.dy - boundingRects.bottom);
2418 _resetOriginOnBottom = false;
2419 }
2420
2421 final double currentX = rawCursorOffset.dx - _relativeOrigin.dx;
2422 final double currentY = rawCursorOffset.dy - _relativeOrigin.dy;
2423 final Offset adjustedOffset = _calculateAdjustedCursorOffset(Offset(currentX, currentY), boundingRects);
2424
2425 if (currentX < boundingRects.left && deltaPosition.dx < 0) {
2426 _resetOriginOnLeft = true;
2427 } else if (currentX > boundingRects.right && deltaPosition.dx > 0) {
2428 _resetOriginOnRight = true;
2429 }
2430 if (currentY < boundingRects.top && deltaPosition.dy < 0) {
2431 _resetOriginOnTop = true;
2432 } else if (currentY > boundingRects.bottom && deltaPosition.dy > 0) {
2433 _resetOriginOnBottom = true;
2434 }
2435
2436 _previousOffset = rawCursorOffset;
2437
2438 return adjustedOffset;
2439 }
2440
2441 /// Sets the screen position of the floating cursor and the text position
2442 /// closest to the cursor.
2443 void setFloatingCursor(FloatingCursorDragState state, Offset boundedOffset, TextPosition lastTextPosition, { double? resetLerpValue }) {
2444 if (state == FloatingCursorDragState.End) {
2445 _relativeOrigin = Offset.zero;
2446 _previousOffset = null;
2447 _shouldResetOrigin = true;
2448 _resetOriginOnBottom = false;
2449 _resetOriginOnTop = false;
2450 _resetOriginOnRight = false;
2451 _resetOriginOnBottom = false;
2452 }
2453 _floatingCursorOn = state != FloatingCursorDragState.End;
2454 _resetFloatingCursorAnimationValue = resetLerpValue;
2455 if (_floatingCursorOn) {
2456 _floatingCursorTextPosition = lastTextPosition;
2457 final double? animationValue = _resetFloatingCursorAnimationValue;
2458 final EdgeInsets sizeAdjustment = animationValue != null
2459 ? EdgeInsets.lerp(_kFloatingCursorSizeIncrease, EdgeInsets.zero, animationValue)!
2460 : _kFloatingCursorSizeIncrease;
2461 _caretPainter.floatingCursorRect = sizeAdjustment.inflateRect(_caretPrototype).shift(boundedOffset);
2462 } else {
2463 _caretPainter.floatingCursorRect = null;
2464 }
2465 _caretPainter.showRegularCaret = _resetFloatingCursorAnimationValue == null;
2466 }
2467
2468 MapEntry<int, Offset> _lineNumberFor(TextPosition startPosition, List<ui.LineMetrics> metrics) {
2469 // TODO(LongCatIsLooong): include line boundaries information in
2470 // ui.LineMetrics, then we can get rid of this.
2471 final Offset offset = _textPainter.getOffsetForCaret(startPosition, Rect.zero);
2472 for (final ui.LineMetrics lineMetrics in metrics) {
2473 if (lineMetrics.baseline > offset.dy) {
2474 return MapEntry<int, Offset>(lineMetrics.lineNumber, Offset(offset.dx, lineMetrics.baseline));
2475 }
2476 }
2477 assert(startPosition.offset == 0, 'unable to find the line for $startPosition');
2478 return MapEntry<int, Offset>(
2479 math.max(0, metrics.length - 1),
2480 Offset(offset.dx, metrics.isNotEmpty ? metrics.last.baseline + metrics.last.descent : 0.0),
2481 );
2482 }
2483
2484 /// Starts a [VerticalCaretMovementRun] at the given location in the text, for
2485 /// handling consecutive vertical caret movements.
2486 ///
2487 /// This can be used to handle consecutive upward/downward arrow key movements
2488 /// in an input field.
2489 ///
2490 /// {@macro flutter.rendering.RenderEditable.verticalArrowKeyMovement}
2491 ///
2492 /// The [VerticalCaretMovementRun.isValid] property indicates whether the text
2493 /// layout has changed and the vertical caret run is invalidated.
2494 ///
2495 /// The caller should typically discard a [VerticalCaretMovementRun] when
2496 /// its [VerticalCaretMovementRun.isValid] becomes false, or on other
2497 /// occasions where the vertical caret run should be interrupted.
2498 VerticalCaretMovementRun startVerticalCaretMovement(TextPosition startPosition) {
2499 final List<ui.LineMetrics> metrics = _textPainter.computeLineMetrics();
2500 final MapEntry<int, Offset> currentLine = _lineNumberFor(startPosition, metrics);
2501 return VerticalCaretMovementRun._(
2502 this,
2503 metrics,
2504 startPosition,
2505 currentLine.key,
2506 currentLine.value,
2507 );
2508 }
2509
2510 void _paintContents(PaintingContext context, Offset offset) {
2511 debugAssertLayoutUpToDate();
2512 final Offset effectiveOffset = offset + _paintOffset;
2513
2514 if (selection != null && !_floatingCursorOn) {
2515 _updateSelectionExtentsVisibility(effectiveOffset);
2516 }
2517
2518 final RenderBox? foregroundChild = _foregroundRenderObject;
2519 final RenderBox? backgroundChild = _backgroundRenderObject;
2520
2521 // The painters paint in the viewport's coordinate space, since the
2522 // textPainter's coordinate space is not known to high level widgets.
2523 if (backgroundChild != null) {
2524 context.paintChild(backgroundChild, offset);
2525 }
2526
2527 _textPainter.paint(context.canvas, effectiveOffset);
2528 paintInlineChildren(context, effectiveOffset);
2529
2530 if (foregroundChild != null) {
2531 context.paintChild(foregroundChild, offset);
2532 }
2533 }
2534
2535 final LayerHandle<LeaderLayer> _leaderLayerHandler = LayerHandle<LeaderLayer>();
2536
2537 void _paintHandleLayers(PaintingContext context, List<TextSelectionPoint> endpoints, Offset offset) {
2538 Offset startPoint = endpoints[0].point;
2539 startPoint = Offset(
2540 clampDouble(startPoint.dx, 0.0, size.width),
2541 clampDouble(startPoint.dy, 0.0, size.height),
2542 );
2543 _leaderLayerHandler.layer = LeaderLayer(link: startHandleLayerLink, offset: startPoint + offset);
2544 context.pushLayer(
2545 _leaderLayerHandler.layer!,
2546 super.paint,
2547 Offset.zero,
2548 );
2549 if (endpoints.length == 2) {
2550 Offset endPoint = endpoints[1].point;
2551 endPoint = Offset(
2552 clampDouble(endPoint.dx, 0.0, size.width),
2553 clampDouble(endPoint.dy, 0.0, size.height),
2554 );
2555 context.pushLayer(
2556 LeaderLayer(link: endHandleLayerLink, offset: endPoint + offset),
2557 super.paint,
2558 Offset.zero,
2559 );
2560 }
2561 }
2562
2563 @override
2564 void applyPaintTransform(RenderBox child, Matrix4 transform) {
2565 if (child == _foregroundRenderObject || child == _backgroundRenderObject) {
2566 return;
2567 }
2568 defaultApplyPaintTransform(child, transform);
2569 }
2570
2571 @override
2572 void paint(PaintingContext context, Offset offset) {
2573 _computeTextMetricsIfNeeded();
2574 if (_hasVisualOverflow && clipBehavior != Clip.none) {
2575 _clipRectLayer.layer = context.pushClipRect(
2576 needsCompositing,
2577 offset,
2578 Offset.zero & size,
2579 _paintContents,
2580 clipBehavior: clipBehavior,
2581 oldLayer: _clipRectLayer.layer,
2582 );
2583 } else {
2584 _clipRectLayer.layer = null;
2585 _paintContents(context, offset);
2586 }
2587 final TextSelection? selection = this.selection;
2588 if (selection != null && selection.isValid) {
2589 _paintHandleLayers(context, getEndpointsForSelection(selection), offset);
2590 }
2591 }
2592
2593 final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
2594
2595 @override
2596 Rect? describeApproximatePaintClip(RenderObject child) {
2597 switch (clipBehavior) {
2598 case Clip.none:
2599 return null;
2600 case Clip.hardEdge:
2601 case Clip.antiAlias:
2602 case Clip.antiAliasWithSaveLayer:
2603 return _hasVisualOverflow ? Offset.zero & size : null;
2604 }
2605 }
2606
2607 @override
2608 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
2609 super.debugFillProperties(properties);
2610 properties.add(ColorProperty('cursorColor', cursorColor));
2611 properties.add(DiagnosticsProperty<ValueNotifier<bool>>('showCursor', showCursor));
2612 properties.add(IntProperty('maxLines', maxLines));
2613 properties.add(IntProperty('minLines', minLines));
2614 properties.add(DiagnosticsProperty<bool>('expands', expands, defaultValue: false));
2615 properties.add(ColorProperty('selectionColor', selectionColor));
2616 properties.add(DiagnosticsProperty<TextScaler>('textScaler', textScaler, defaultValue: TextScaler.noScaling));
2617 properties.add(DiagnosticsProperty<Locale>('locale', locale, defaultValue: null));
2618 properties.add(DiagnosticsProperty<TextSelection>('selection', selection));
2619 properties.add(DiagnosticsProperty<ViewportOffset>('offset', offset));
2620 }
2621
2622 @override
2623 List<DiagnosticsNode> debugDescribeChildren() {
2624 return <DiagnosticsNode>[
2625 if (text != null)
2626 text!.toDiagnosticsNode(
2627 name: 'text',
2628 style: DiagnosticsTreeStyle.transition,
2629 ),
2630 ];
2631 }
2632}
2633
2634class _RenderEditableCustomPaint extends RenderBox {
2635 _RenderEditableCustomPaint({
2636 RenderEditablePainter? painter,
2637 }) : _painter = painter,
2638 super();
2639
2640 @override
2641 RenderEditable? get parent => super.parent as RenderEditable?;
2642
2643 @override
2644 bool get isRepaintBoundary => true;
2645
2646 @override
2647 bool get sizedByParent => true;
2648
2649 RenderEditablePainter? get painter => _painter;
2650 RenderEditablePainter? _painter;
2651 set painter(RenderEditablePainter? newValue) {
2652 if (newValue == painter) {
2653 return;
2654 }
2655
2656 final RenderEditablePainter? oldPainter = painter;
2657 _painter = newValue;
2658
2659 if (newValue?.shouldRepaint(oldPainter) ?? true) {
2660 markNeedsPaint();
2661 }
2662
2663 if (attached) {
2664 oldPainter?.removeListener(markNeedsPaint);
2665 newValue?.addListener(markNeedsPaint);
2666 }
2667 }
2668
2669 @override
2670 void paint(PaintingContext context, Offset offset) {
2671 final RenderEditable? parent = this.parent;
2672 assert(parent != null);
2673 final RenderEditablePainter? painter = this.painter;
2674 if (painter != null && parent != null) {
2675 parent._computeTextMetricsIfNeeded();
2676 painter.paint(context.canvas, size, parent);
2677 }
2678 }
2679
2680 @override
2681 void attach(PipelineOwner owner) {
2682 super.attach(owner);
2683 _painter?.addListener(markNeedsPaint);
2684 }
2685
2686 @override
2687 void detach() {
2688 _painter?.removeListener(markNeedsPaint);
2689 super.detach();
2690 }
2691
2692 @override
2693 @protected
2694 Size computeDryLayout(covariant BoxConstraints constraints) => constraints.biggest;
2695}
2696
2697/// An interface that paints within a [RenderEditable]'s bounds, above or
2698/// beneath its text content.
2699///
2700/// This painter is typically used for painting auxiliary content that depends
2701/// on text layout metrics (for instance, for painting carets and text highlight
2702/// blocks). It can paint independently from its [RenderEditable], allowing it
2703/// to repaint without triggering a repaint on the entire [RenderEditable] stack
2704/// when only auxiliary content changes (e.g. a blinking cursor) are present. It
2705/// will be scheduled to repaint when:
2706///
2707/// * It's assigned to a new [RenderEditable] (replacing a prior
2708/// [RenderEditablePainter]) and the [shouldRepaint] method returns true.
2709/// * Any of the [RenderEditable]s it is attached to repaints.
2710/// * The [notifyListeners] method is called, which typically happens when the
2711/// painter's attributes change.
2712///
2713/// See also:
2714///
2715/// * [RenderEditable.foregroundPainter], which takes a [RenderEditablePainter]
2716/// and sets it as the foreground painter of the [RenderEditable].
2717/// * [RenderEditable.painter], which takes a [RenderEditablePainter]
2718/// and sets it as the background painter of the [RenderEditable].
2719/// * [CustomPainter], a similar class which paints within a [RenderCustomPaint].
2720abstract class RenderEditablePainter extends ChangeNotifier {
2721 /// Determines whether repaint is needed when a new [RenderEditablePainter]
2722 /// is provided to a [RenderEditable].
2723 ///
2724 /// If the new instance represents different information than the old
2725 /// instance, then the method should return true, otherwise it should return
2726 /// false. When [oldDelegate] is null, this method should always return true
2727 /// unless the new painter initially does not paint anything.
2728 ///
2729 /// If the method returns false, then the [paint] call might be optimized
2730 /// away. However, the [paint] method will get called whenever the
2731 /// [RenderEditable]s it attaches to repaint, even if [shouldRepaint] returns
2732 /// false.
2733 bool shouldRepaint(RenderEditablePainter? oldDelegate);
2734
2735 /// Paints within the bounds of a [RenderEditable].
2736 ///
2737 /// The given [Canvas] has the same coordinate space as the [RenderEditable],
2738 /// which may be different from the coordinate space the [RenderEditable]'s
2739 /// [TextPainter] uses, when the text moves inside the [RenderEditable].
2740 ///
2741 /// Paint operations performed outside of the region defined by the [canvas]'s
2742 /// origin and the [size] parameter may get clipped, when [RenderEditable]'s
2743 /// [RenderEditable.clipBehavior] is not [Clip.none].
2744 void paint(Canvas canvas, Size size, RenderEditable renderEditable);
2745}
2746
2747class _TextHighlightPainter extends RenderEditablePainter {
2748 _TextHighlightPainter({
2749 TextRange? highlightedRange,
2750 Color? highlightColor,
2751 }) : _highlightedRange = highlightedRange,
2752 _highlightColor = highlightColor;
2753
2754 final Paint highlightPaint = Paint();
2755
2756 Color? get highlightColor => _highlightColor;
2757 Color? _highlightColor;
2758 set highlightColor(Color? newValue) {
2759 if (newValue == _highlightColor) {
2760 return;
2761 }
2762 _highlightColor = newValue;
2763 notifyListeners();
2764 }
2765
2766 TextRange? get highlightedRange => _highlightedRange;
2767 TextRange? _highlightedRange;
2768 set highlightedRange(TextRange? newValue) {
2769 if (newValue == _highlightedRange) {
2770 return;
2771 }
2772 _highlightedRange = newValue;
2773 notifyListeners();
2774 }
2775
2776 /// Controls how tall the selection highlight boxes are computed to be.
2777 ///
2778 /// See [ui.BoxHeightStyle] for details on available styles.
2779 ui.BoxHeightStyle get selectionHeightStyle => _selectionHeightStyle;
2780 ui.BoxHeightStyle _selectionHeightStyle = ui.BoxHeightStyle.tight;
2781 set selectionHeightStyle(ui.BoxHeightStyle value) {
2782 if (_selectionHeightStyle == value) {
2783 return;
2784 }
2785 _selectionHeightStyle = value;
2786 notifyListeners();
2787 }
2788
2789 /// Controls how wide the selection highlight boxes are computed to be.
2790 ///
2791 /// See [ui.BoxWidthStyle] for details on available styles.
2792 ui.BoxWidthStyle get selectionWidthStyle => _selectionWidthStyle;
2793 ui.BoxWidthStyle _selectionWidthStyle = ui.BoxWidthStyle.tight;
2794 set selectionWidthStyle(ui.BoxWidthStyle value) {
2795 if (_selectionWidthStyle == value) {
2796 return;
2797 }
2798 _selectionWidthStyle = value;
2799 notifyListeners();
2800 }
2801
2802 @override
2803 void paint(Canvas canvas, Size size, RenderEditable renderEditable) {
2804 final TextRange? range = highlightedRange;
2805 final Color? color = highlightColor;
2806 if (range == null || color == null || range.isCollapsed) {
2807 return;
2808 }
2809
2810 highlightPaint.color = color;
2811 final TextPainter textPainter = renderEditable._textPainter;
2812 final List<TextBox> boxes = textPainter.getBoxesForSelection(
2813 TextSelection(baseOffset: range.start, extentOffset: range.end),
2814 boxHeightStyle: selectionHeightStyle,
2815 boxWidthStyle: selectionWidthStyle,
2816 );
2817
2818 for (final TextBox box in boxes) {
2819 canvas.drawRect(
2820 box.toRect().shift(renderEditable._paintOffset)
2821 .intersect(Rect.fromLTWH(0, 0, textPainter.width, textPainter.height)),
2822 highlightPaint,
2823 );
2824 }
2825 }
2826
2827 @override
2828 bool shouldRepaint(RenderEditablePainter? oldDelegate) {
2829 if (identical(oldDelegate, this)) {
2830 return false;
2831 }
2832 if (oldDelegate == null) {
2833 return highlightColor != null && highlightedRange != null;
2834 }
2835 return oldDelegate is! _TextHighlightPainter
2836 || oldDelegate.highlightColor != highlightColor
2837 || oldDelegate.highlightedRange != highlightedRange
2838 || oldDelegate.selectionHeightStyle != selectionHeightStyle
2839 || oldDelegate.selectionWidthStyle != selectionWidthStyle;
2840 }
2841}
2842
2843class _CaretPainter extends RenderEditablePainter {
2844 _CaretPainter();
2845
2846 bool get shouldPaint => _shouldPaint;
2847 bool _shouldPaint = true;
2848 set shouldPaint(bool value) {
2849 if (shouldPaint == value) {
2850 return;
2851 }
2852 _shouldPaint = value;
2853 notifyListeners();
2854 }
2855
2856 // This is directly manipulated by the RenderEditable during
2857 // setFloatingCursor.
2858 //
2859 // When changing this value, the caller is responsible for ensuring that
2860 // listeners are notified.
2861 bool showRegularCaret = false;
2862
2863 final Paint caretPaint = Paint();
2864 late final Paint floatingCursorPaint = Paint();
2865
2866 Color? get caretColor => _caretColor;
2867 Color? _caretColor;
2868 set caretColor(Color? value) {
2869 if (caretColor?.value == value?.value) {
2870 return;
2871 }
2872
2873 _caretColor = value;
2874 notifyListeners();
2875 }
2876
2877 Radius? get cursorRadius => _cursorRadius;
2878 Radius? _cursorRadius;
2879 set cursorRadius(Radius? value) {
2880 if (_cursorRadius == value) {
2881 return;
2882 }
2883 _cursorRadius = value;
2884 notifyListeners();
2885 }
2886
2887 Offset get cursorOffset => _cursorOffset;
2888 Offset _cursorOffset = Offset.zero;
2889 set cursorOffset(Offset value) {
2890 if (_cursorOffset == value) {
2891 return;
2892 }
2893 _cursorOffset = value;
2894 notifyListeners();
2895 }
2896
2897 Color? get backgroundCursorColor => _backgroundCursorColor;
2898 Color? _backgroundCursorColor;
2899 set backgroundCursorColor(Color? value) {
2900 if (backgroundCursorColor?.value == value?.value) {
2901 return;
2902 }
2903
2904 _backgroundCursorColor = value;
2905 if (showRegularCaret) {
2906 notifyListeners();
2907 }
2908 }
2909
2910 Rect? get floatingCursorRect => _floatingCursorRect;
2911 Rect? _floatingCursorRect;
2912 set floatingCursorRect(Rect? value) {
2913 if (_floatingCursorRect == value) {
2914 return;
2915 }
2916 _floatingCursorRect = value;
2917 notifyListeners();
2918 }
2919
2920 void paintRegularCursor(Canvas canvas, RenderEditable renderEditable, Color caretColor, TextPosition textPosition) {
2921 final Rect integralRect = renderEditable.getLocalRectForCaret(textPosition);
2922 if (shouldPaint) {
2923 if (floatingCursorRect != null) {
2924 final double distanceSquared = (floatingCursorRect!.center - integralRect.center).distanceSquared;
2925 if (distanceSquared < _kShortestDistanceSquaredWithFloatingAndRegularCursors) {
2926 return;
2927 }
2928 }
2929 final Radius? radius = cursorRadius;
2930 caretPaint.color = caretColor;
2931 if (radius == null) {
2932 canvas.drawRect(integralRect, caretPaint);
2933 } else {
2934 final RRect caretRRect = RRect.fromRectAndRadius(integralRect, radius);
2935 canvas.drawRRect(caretRRect, caretPaint);
2936 }
2937 }
2938 }
2939
2940 @override
2941 void paint(Canvas canvas, Size size, RenderEditable renderEditable) {
2942 // Compute the caret location even when `shouldPaint` is false.
2943
2944 final TextSelection? selection = renderEditable.selection;
2945
2946 // TODO(LongCatIsLooong): skip painting caret when selection is (-1, -1): https://github.com/flutter/flutter/issues/79495
2947 if (selection == null || !selection.isCollapsed) {
2948 return;
2949 }
2950
2951 final Rect? floatingCursorRect = this.floatingCursorRect;
2952
2953 final Color? caretColor = floatingCursorRect == null
2954 ? this.caretColor
2955 : showRegularCaret ? backgroundCursorColor : null;
2956 final TextPosition caretTextPosition = floatingCursorRect == null
2957 ? selection.extent
2958 : renderEditable._floatingCursorTextPosition;
2959
2960 if (caretColor != null) {
2961 paintRegularCursor(canvas, renderEditable, caretColor, caretTextPosition);
2962 }
2963
2964 final Color? floatingCursorColor = this.caretColor?.withOpacity(0.75);
2965 // Floating Cursor.
2966 if (floatingCursorRect == null || floatingCursorColor == null || !shouldPaint) {
2967 return;
2968 }
2969
2970 canvas.drawRRect(
2971 RRect.fromRectAndRadius(floatingCursorRect, _kFloatingCursorRadius),
2972 floatingCursorPaint..color = floatingCursorColor,
2973 );
2974 }
2975
2976 @override
2977 bool shouldRepaint(RenderEditablePainter? oldDelegate) {
2978 if (identical(this, oldDelegate)) {
2979 return false;
2980 }
2981
2982 if (oldDelegate == null) {
2983 return shouldPaint;
2984 }
2985 return oldDelegate is! _CaretPainter
2986 || oldDelegate.shouldPaint != shouldPaint
2987 || oldDelegate.showRegularCaret != showRegularCaret
2988 || oldDelegate.caretColor != caretColor
2989 || oldDelegate.cursorRadius != cursorRadius
2990 || oldDelegate.cursorOffset != cursorOffset
2991 || oldDelegate.backgroundCursorColor != backgroundCursorColor
2992 || oldDelegate.floatingCursorRect != floatingCursorRect;
2993 }
2994}
2995
2996class _CompositeRenderEditablePainter extends RenderEditablePainter {
2997 _CompositeRenderEditablePainter({ required this.painters });
2998
2999 final List<RenderEditablePainter> painters;
3000
3001 @override
3002 void addListener(VoidCallback listener) {
3003 for (final RenderEditablePainter painter in painters) {
3004 painter.addListener(listener);
3005 }
3006 }
3007
3008 @override
3009 void removeListener(VoidCallback listener) {
3010 for (final RenderEditablePainter painter in painters) {
3011 painter.removeListener(listener);
3012 }
3013 }
3014
3015 @override
3016 void paint(Canvas canvas, Size size, RenderEditable renderEditable) {
3017 for (final RenderEditablePainter painter in painters) {
3018 painter.paint(canvas, size, renderEditable);
3019 }
3020 }
3021
3022 @override
3023 bool shouldRepaint(RenderEditablePainter? oldDelegate) {
3024 if (identical(oldDelegate, this)) {
3025 return false;
3026 }
3027 if (oldDelegate is! _CompositeRenderEditablePainter || oldDelegate.painters.length != painters.length) {
3028 return true;
3029 }
3030
3031 final Iterator<RenderEditablePainter> oldPainters = oldDelegate.painters.iterator;
3032 final Iterator<RenderEditablePainter> newPainters = painters.iterator;
3033 while (oldPainters.moveNext() && newPainters.moveNext()) {
3034 if (newPainters.current.shouldRepaint(oldPainters.current)) {
3035 return true;
3036 }
3037 }
3038
3039 return false;
3040 }
3041}
3042