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