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