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