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