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

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com