LCOV - code coverage report
Current view: top level - editing - mongol_render_editable.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 1078 1521 70.9 %
Date: 2021-08-02 17:55:49 Functions: 0 0 -

          Line data    Source code
       1             : // Copyright 2014 The Flutter Authors.
       2             : // Copyright 2021 Suragch.
       3             : // All rights reserved.
       4             : // Use of this source code is governed by a BSD-style license that can be
       5             : // found in the LICENSE file.
       6             : 
       7             : import 'dart:collection';
       8             : import 'dart:math' as math;
       9             : 
      10             : import 'package:characters/characters.dart';
      11             : import 'package:flutter/foundation.dart';
      12             : import 'package:flutter/gestures.dart';
      13             : import 'package:flutter/material.dart';
      14             : import 'package:flutter/rendering.dart';
      15             : import 'package:flutter/services.dart';
      16             : import 'package:mongol/src/base/mongol_text_align.dart';
      17             : import 'package:mongol/src/base/mongol_text_painter.dart';
      18             : 
      19             : // ignore_for_file: deprecated_member_use_from_same_package
      20             : // ignore_for_file: todo
      21             : 
      22             : const double _kCaretGap = 1.0; // pixels
      23             : const double _kCaretWidthOffset = 2.0; // pixels
      24             : 
      25             : /// Signature for the callback that reports when the user changes the selection
      26             : /// (including the cursor location).
      27             : ///
      28             : /// Used by [MongolRenderEditable.onSelectionChanged].
      29             : @Deprecated(
      30             :   'Signature of a deprecated class method, '
      31             :   'textSelectionDelegate.userUpdateTextEditingValue. '
      32             :   'This feature was deprecated after v1.26.0-17.2.pre.',
      33             : )
      34             : typedef MongolSelectionChangedHandler = void Function(
      35             :   TextSelection selection,
      36             :   MongolRenderEditable renderObject,
      37             :   SelectionChangedCause cause,
      38             : );
      39             : 
      40             : // Check if the given code unit is a white space or separator
      41             : // character.
      42             : //
      43             : // Includes newline characters from ASCII and separators from the
      44             : // [unicode separator category](https://www.compart.com/en/unicode/category/Zs)
      45           3 : bool _isWhitespace(int codeUnit) {
      46             :   switch (codeUnit) {
      47           3 :     case 0x9: // horizontal tab
      48           3 :     case 0xA: // line feed
      49           3 :     case 0xB: // vertical tab
      50           3 :     case 0xC: // form feed
      51           3 :     case 0xD: // carriage return
      52           3 :     case 0x1C: // file separator
      53           3 :     case 0x1D: // group separator
      54           3 :     case 0x1E: // record separator
      55           3 :     case 0x1F: // unit separator
      56           3 :     case 0x20: // space
      57           3 :     case 0xA0: // no-break space
      58           3 :     case 0x1680: // ogham space mark
      59           3 :     case 0x2000: // en quad
      60           3 :     case 0x2001: // em quad
      61           3 :     case 0x2002: // en space
      62           3 :     case 0x2003: // em space
      63           3 :     case 0x2004: // three-per-em space
      64           3 :     case 0x2005: // four-er-em space
      65           3 :     case 0x2006: // six-per-em space
      66           3 :     case 0x2007: // figure space
      67           3 :     case 0x2008: // punctuation space
      68           3 :     case 0x2009: // thin space
      69           3 :     case 0x200A: // hair space
      70           3 :     case 0x202F: // narrow no-break space
      71           3 :     case 0x205F: // medium mathematical space
      72           3 :     case 0x3000: // ideographic space
      73             :       break;
      74             :     default:
      75             :       return false;
      76             :   }
      77             :   return true;
      78             : }
      79             : 
      80             : /// Displays some text in a scrollable container with a potentially blinking
      81             : /// cursor and with gesture recognizers.
      82             : ///
      83             : /// This is the renderer for an editable vertical text field. It does not
      84             : /// directly provide a means of editing the text, but it does handle text
      85             : /// selection and manipulation of the text cursor.
      86             : ///
      87             : /// The [text] is displayed, scrolled by the given [offset], aligned according
      88             : /// to [textAlign]. The [maxLines] property controls whether the text displays
      89             : /// on one line or many. The [selection], if it is not collapsed, is painted in
      90             : /// the [selectionColor]. If it _is_ collapsed, then it represents the cursor
      91             : /// position. The cursor is shown while [showCursor] is true. It is painted in
      92             : /// the [cursorColor].
      93             : ///
      94             : /// If, when the render object paints, the caret is found to have changed
      95             : /// location, [onCaretChanged] is called.
      96             : ///
      97             : /// Keyboard handling, IME handling, scrolling, toggling the [showCursor] value
      98             : /// to actually blink the cursor, and other features not mentioned above are the
      99             : /// responsibility of higher layers and not handled by this object.
     100             : class MongolRenderEditable extends RenderBox
     101             :     with RelayoutWhenSystemFontsChangeMixin {
     102             :   /// Creates a render object that implements the visual aspects of a text field.
     103             :   ///
     104             :   /// The [textAlign] argument must not be null. It defaults to
     105             :   /// [MongolTextAlign.top].
     106             :   ///
     107             :   /// If [showCursor] is not specified, then it defaults to hiding the cursor.
     108             :   ///
     109             :   /// The [maxLines] property can be set to null to remove the restriction on
     110             :   /// the number of lines. By default, it is 1, meaning this is a single-line
     111             :   /// text field. If it is not null, it must be greater than zero.
     112             :   ///
     113             :   /// The [offset] is required and must not be null. You can use
     114             :   /// [ViewportOffset.zero] if you have no need for scrolling.
     115           3 :   MongolRenderEditable({
     116             :     TextSpan? text,
     117             :     MongolTextAlign textAlign = MongolTextAlign.top,
     118             :     Color? cursorColor,
     119             :     ValueNotifier<bool>? showCursor,
     120             :     bool? hasFocus,
     121             :     required LayerLink startHandleLayerLink,
     122             :     required LayerLink endHandleLayerLink,
     123             :     int? maxLines = 1,
     124             :     int? minLines,
     125             :     bool expands = false,
     126             :     Color? selectionColor,
     127             :     double textScaleFactor = 1.0,
     128             :     TextSelection? selection,
     129             :     required ViewportOffset offset,
     130             :     @Deprecated(
     131             :       'Uses the textSelectionDelegate.userUpdateTextEditingValue instead. '
     132             :       'This feature was deprecated after v1.26.0-17.2.pre.',
     133             :     )
     134             :         this.onSelectionChanged,
     135             :     this.onCaretChanged,
     136             :     this.ignorePointer = false,
     137             :     bool readOnly = false,
     138             :     bool forceLine = true,
     139             :     String obscuringCharacter = '•',
     140             :     bool obscureText = false,
     141             :     double? cursorWidth,
     142             :     double cursorHeight = 1.0,
     143             :     Radius? cursorRadius,
     144             :     Offset cursorOffset = Offset.zero,
     145             :     double devicePixelRatio = 1.0,
     146             :     bool? enableInteractiveSelection,
     147             :     Clip clipBehavior = Clip.hardEdge,
     148             :     required this.textSelectionDelegate,
     149             :     MongolRenderEditablePainter? painter,
     150             :     MongolRenderEditablePainter? foregroundPainter,
     151           3 :   })  : assert(maxLines == null || maxLines > 0),
     152           1 :         assert(minLines == null || minLines > 0),
     153             :         assert(
     154           1 :           (maxLines == null) || (minLines == null) || (maxLines >= minLines),
     155             :           "minLines can't be greater than maxLines",
     156             :         ),
     157             :         assert(
     158           0 :           !expands || (maxLines == null && minLines == null),
     159             :           'minLines and maxLines must be null when expands is true.',
     160             :         ),
     161           9 :         assert(obscuringCharacter.characters.length == 1),
     162           0 :         assert(cursorWidth == null || cursorWidth >= 0.0),
     163           3 :         assert(cursorHeight >= 0.0),
     164           3 :         _textPainter = MongolTextPainter(
     165             :           text: text,
     166             :           textAlign: textAlign,
     167             :           textScaleFactor: textScaleFactor,
     168             :         ),
     169           1 :         _showCursor = showCursor ?? ValueNotifier<bool>(false),
     170             :         _maxLines = maxLines,
     171             :         _minLines = minLines,
     172             :         _expands = expands,
     173             :         _selection = selection,
     174             :         _offset = offset,
     175             :         _cursorWidth = cursorWidth,
     176             :         _cursorHeight = cursorHeight,
     177             :         _enableInteractiveSelection = enableInteractiveSelection,
     178             :         _devicePixelRatio = devicePixelRatio,
     179             :         _startHandleLayerLink = startHandleLayerLink,
     180             :         _endHandleLayerLink = endHandleLayerLink,
     181             :         _obscuringCharacter = obscuringCharacter,
     182             :         _obscureText = obscureText,
     183             :         _readOnly = readOnly,
     184             :         _forceLine = forceLine,
     185             :         _clipBehavior = clipBehavior {
     186           6 :     assert(!_showCursor.value || cursorColor != null);
     187           3 :     this.hasFocus = hasFocus ?? false;
     188             : 
     189           6 :     _selectionPainter.highlightColor = selectionColor;
     190           6 :     _selectionPainter.highlightedRange = selection;
     191             : 
     192           6 :     _caretPainter.caretColor = cursorColor;
     193           6 :     _caretPainter.cursorRadius = cursorRadius;
     194           6 :     _caretPainter.cursorOffset = cursorOffset;
     195             : 
     196           3 :     _updateForegroundPainter(foregroundPainter);
     197           3 :     _updatePainter(painter);
     198             :   }
     199             : 
     200             :   /// Child render objects
     201             :   _MongolRenderEditableCustomPaint? _foregroundRenderObject;
     202             :   _MongolRenderEditableCustomPaint? _backgroundRenderObject;
     203             : 
     204           3 :   void _updateForegroundPainter(MongolRenderEditablePainter? newPainter) {
     205             :     final effectivePainter = (newPainter == null)
     206           3 :         ? _builtInForegroundPainters
     207           1 :         : _CompositeRenderEditablePainter(
     208           1 :             painters: <MongolRenderEditablePainter>[
     209           1 :               _builtInForegroundPainters,
     210             :               newPainter,
     211             :             ],
     212             :           );
     213             : 
     214           3 :     if (_foregroundRenderObject == null) {
     215             :       final foregroundRenderObject =
     216           3 :           _MongolRenderEditableCustomPaint(painter: effectivePainter);
     217           3 :       adoptChild(foregroundRenderObject);
     218           3 :       _foregroundRenderObject = foregroundRenderObject;
     219             :     } else {
     220           2 :       _foregroundRenderObject?.painter = effectivePainter;
     221             :     }
     222           3 :     _foregroundPainter = newPainter;
     223             :   }
     224             : 
     225             :   /// The [MongolRenderEditablePainter] to use for painting above this
     226             :   /// [MongolRenderEditable]'s text content.
     227             :   ///
     228             :   /// The new [MongolRenderEditablePainter] will replace the previously specified
     229             :   /// foreground painter, and schedule a repaint if the new painter's
     230             :   /// `shouldRepaint` method returns true.
     231           0 :   MongolRenderEditablePainter? get foregroundPainter => _foregroundPainter;
     232             :   MongolRenderEditablePainter? _foregroundPainter;
     233           1 :   set foregroundPainter(MongolRenderEditablePainter? newPainter) {
     234           2 :     if (newPainter == _foregroundPainter) return;
     235           1 :     _updateForegroundPainter(newPainter);
     236             :   }
     237             : 
     238           3 :   void _updatePainter(MongolRenderEditablePainter? newPainter) {
     239             :     final effectivePainter = (newPainter == null)
     240           3 :         ? _builtInPainters
     241           1 :         : _CompositeRenderEditablePainter(
     242           1 :             painters: <MongolRenderEditablePainter>[
     243           1 :               _builtInPainters,
     244             :               newPainter,
     245             :             ],
     246             :           );
     247             : 
     248           3 :     if (_backgroundRenderObject == null) {
     249             :       final backgroundRenderObject =
     250           3 :           _MongolRenderEditableCustomPaint(painter: effectivePainter);
     251           3 :       adoptChild(backgroundRenderObject);
     252           3 :       _backgroundRenderObject = backgroundRenderObject;
     253             :     } else {
     254           2 :       _backgroundRenderObject?.painter = effectivePainter;
     255             :     }
     256           3 :     _painter = newPainter;
     257             :   }
     258             : 
     259             :   /// Sets the [MongolRenderEditablePainter] to use for painting beneath this
     260             :   /// [MongolRenderEditable]'s text content.
     261             :   ///
     262             :   /// The new [MongolRenderEditablePainter] will replace the previously specified
     263             :   /// painter, and schedule a repaint if the new painter's `shouldRepaint`
     264             :   /// method returns true.
     265           0 :   MongolRenderEditablePainter? get painter => _painter;
     266             :   MongolRenderEditablePainter? _painter;
     267           1 :   set painter(MongolRenderEditablePainter? newPainter) {
     268           2 :     if (newPainter == _painter) return;
     269           1 :     _updatePainter(newPainter);
     270             :   }
     271             : 
     272             :   // Caret painters:
     273           9 :   late final _CaretPainter _caretPainter = _CaretPainter(_onCaretChanged);
     274             : 
     275             :   // Text Highlight painters:
     276             :   final _TextHighlightPainter _selectionPainter = _TextHighlightPainter();
     277             : 
     278           3 :   _CompositeRenderEditablePainter get _builtInForegroundPainters =>
     279           6 :       _cachedBuiltInForegroundPainters ??= _createBuiltInForegroundPainters();
     280             :   _CompositeRenderEditablePainter? _cachedBuiltInForegroundPainters;
     281           3 :   _CompositeRenderEditablePainter _createBuiltInForegroundPainters() {
     282           3 :     return _CompositeRenderEditablePainter(
     283           3 :       painters: <MongolRenderEditablePainter>[
     284           3 :         _caretPainter,
     285             :       ],
     286             :     );
     287             :   }
     288             : 
     289           3 :   _CompositeRenderEditablePainter get _builtInPainters =>
     290           6 :       _cachedBuiltInPainters ??= _createBuiltInPainters();
     291             :   _CompositeRenderEditablePainter? _cachedBuiltInPainters;
     292           3 :   _CompositeRenderEditablePainter _createBuiltInPainters() {
     293           3 :     return _CompositeRenderEditablePainter(
     294           3 :       painters: <MongolRenderEditablePainter>[
     295           3 :         _selectionPainter,
     296             :       ],
     297             :     );
     298             :   }
     299             : 
     300             :   /// Called when the selection changes.
     301             :   ///
     302             :   /// If this is null, then selection changes will be ignored.
     303             :   @Deprecated(
     304             :     'Uses the textSelectionDelegate.userUpdateTextEditingValue instead. '
     305             :     'This feature was deprecated after v1.26.0-17.2.pre.',
     306             :   )
     307             :   MongolSelectionChangedHandler? onSelectionChanged;
     308             : 
     309             :   double? _textLayoutLastMaxHeight;
     310             :   double? _textLayoutLastMinHeight;
     311             : 
     312             :   Rect? _lastCaretRect;
     313             : 
     314             :   /// Called during the paint phase when the caret location changes.
     315             :   CaretChangedHandler? onCaretChanged;
     316           3 :   void _onCaretChanged(Rect caretRect) {
     317          11 :     if (_lastCaretRect != caretRect) onCaretChanged?.call(caretRect);
     318           6 :     _lastCaretRect = (onCaretChanged == null) ? null : caretRect;
     319             :   }
     320             : 
     321             :   /// Whether the [handleEvent] will propagate pointer events to selection
     322             :   /// handlers.
     323             :   ///
     324             :   /// If this property is true, the [handleEvent] assumes that this renderer
     325             :   /// will be notified of input gestures via [handleTapDown], [handleTap],
     326             :   /// [handleDoubleTap], and [handleLongPress].
     327             :   ///
     328             :   /// If there are any gesture recognizers in the text span, the [handleEvent]
     329             :   /// will still propagate pointer events to those recognizers.
     330             :   ///
     331             :   /// The default value of this property is false.
     332             :   bool ignorePointer;
     333             : 
     334             :   /// The pixel ratio of the current device.
     335             :   ///
     336             :   /// Should be obtained by querying MediaQuery for the devicePixelRatio.
     337           4 :   double get devicePixelRatio => _devicePixelRatio;
     338             :   double _devicePixelRatio;
     339           2 :   set devicePixelRatio(double value) {
     340           4 :     if (devicePixelRatio == value) return;
     341           0 :     _devicePixelRatio = value;
     342           0 :     markNeedsTextLayout();
     343             :   }
     344             : 
     345             :   /// Character used for obscuring text if [obscureText] is true.
     346             :   ///
     347             :   /// Must have a length of exactly one.
     348           6 :   String get obscuringCharacter => _obscuringCharacter;
     349             :   String _obscuringCharacter;
     350           2 :   set obscuringCharacter(String value) {
     351           4 :     if (_obscuringCharacter == value) {
     352             :       return;
     353             :     }
     354           0 :     assert(value.characters.length == 1);
     355           0 :     _obscuringCharacter = value;
     356           0 :     markNeedsLayout();
     357             :   }
     358             : 
     359             :   /// Whether to hide the text being edited (e.g., for passwords).
     360           6 :   bool get obscureText => _obscureText;
     361             :   bool _obscureText;
     362           2 :   set obscureText(bool value) {
     363           4 :     if (_obscureText == value) return;
     364           1 :     _obscureText = value;
     365           1 :     markNeedsSemanticsUpdate();
     366             :   }
     367             : 
     368             :   /// The object that controls the text selection, used by this render object
     369             :   /// for implementing cut, copy, and paste keyboard shortcuts.
     370             :   ///
     371             :   /// It must not be null. It will make cut, copy and paste functionality work
     372             :   /// with the most recently set [TextSelectionDelegate].
     373             :   TextSelectionDelegate textSelectionDelegate;
     374             : 
     375             :   /// Track whether position of the start of the selected text is within the viewport.
     376             :   ///
     377             :   /// For example, if the text contains "Hello World", and the user selects
     378             :   /// "Hello", then scrolls so only "World" is visible, this will become false.
     379             :   /// If the user scrolls back so that the "H" is visible again, this will
     380             :   /// become true.
     381             :   ///
     382             :   /// This bool indicates whether the text is scrolled so that the handle is
     383             :   /// inside the text field viewport, as opposed to whether it is actually
     384             :   /// visible on the screen.
     385           1 :   ValueListenable<bool> get selectionStartInViewport =>
     386           1 :       _selectionStartInViewport;
     387             :   final ValueNotifier<bool> _selectionStartInViewport =
     388             :       ValueNotifier<bool>(true);
     389             : 
     390             :   /// Track whether position of the end of the selected text is within the viewport.
     391             :   ///
     392             :   /// For example, if the text contains "Hello World", and the user selects
     393             :   /// "World", then scrolls so only "Hello" is visible, this will become
     394             :   /// 'false'. If the user scrolls back so that the "d" is visible again, this
     395             :   /// will become 'true'.
     396             :   ///
     397             :   /// This bool indicates whether the text is scrolled so that the handle is
     398             :   /// inside the text field viewport, as opposed to whether it is actually
     399             :   /// visible on the screen.
     400           2 :   ValueListenable<bool> get selectionEndInViewport => _selectionEndInViewport;
     401             :   final ValueNotifier<bool> _selectionEndInViewport = ValueNotifier<bool>(true);
     402             : 
     403           3 :   void _updateSelectionExtentsVisibility(Offset effectiveOffset) {
     404           3 :     assert(selection != null);
     405           6 :     final visibleRegion = Offset.zero & size;
     406             : 
     407           6 :     final startOffset = _textPainter.getOffsetForCaret(
     408          15 :       TextPosition(offset: selection!.start, affinity: selection!.affinity),
     409           3 :       _caretPrototype,
     410             :     );
     411             :     // TODO(justinmc): https://github.com/flutter/flutter/issues/31495
     412             :     // Check if the selection is visible with an approximation because a
     413             :     // difference between rounded and unrounded values causes the caret to be
     414             :     // reported as having a slightly (< 0.5) negative y offset. This rounding
     415             :     // happens in paragraph.cc's layout and TextPainer's
     416             :     // _applyFloatingPointHack. Ideally, the rounding mismatch will be fixed and
     417             :     // this can be changed to be a strict check instead of an approximation.
     418             :     const visibleRegionSlop = 0.5;
     419           6 :     _selectionStartInViewport.value = visibleRegion
     420           3 :         .inflate(visibleRegionSlop)
     421           6 :         .contains(startOffset + effectiveOffset);
     422             : 
     423           6 :     final endOffset = _textPainter.getOffsetForCaret(
     424          15 :       TextPosition(offset: selection!.end, affinity: selection!.affinity),
     425           3 :       _caretPrototype,
     426             :     );
     427           6 :     _selectionEndInViewport.value = visibleRegion
     428           3 :         .inflate(visibleRegionSlop)
     429           6 :         .contains(endOffset + effectiveOffset);
     430             :   }
     431             : 
     432             :   // Holds the last cursor location the user selected in the case the user tries
     433             :   // to select horizontally past the end or beginning of the field. If they do,
     434             :   // then we need to keep the old cursor location so that we can go back to it
     435             :   // if they change their minds. Only used for moving selection right and left
     436             :   // in a multiline text field when selecting using the keyboard.
     437             :   int _cursorResetLocation = -1;
     438             : 
     439             :   // Whether we should reset the location of the cursor in the case the user
     440             :   // tries to select horizontally past the end or beginning of the field. If they
     441             :   // do, then we need to keep the old cursor location so that we can go back to
     442             :   // it if they change their minds. Only used for resetting selection left and
     443             :   // right in a multiline text field when selecting using the keyboard.
     444             :   bool _wasSelectingHorizontallyWithKeyboard = false;
     445             : 
     446           3 :   void _setTextEditingValue(
     447             :       TextEditingValue newValue, SelectionChangedCause cause) {
     448           6 :     textSelectionDelegate.textEditingValue = newValue;
     449           6 :     textSelectionDelegate.userUpdateTextEditingValue(newValue, cause);
     450             :   }
     451             : 
     452           3 :   void _setSelection(TextSelection nextSelection, SelectionChangedCause cause) {
     453           3 :     if (nextSelection.isValid) {
     454             :       // The nextSelection is calculated based on _plainText, which can be out
     455             :       // of sync with the textSelectionDelegate.textEditingValue by one frame.
     456             :       // This is due to the render editable and editable text handle pointer
     457             :       // event separately. If the editable text changes the text during the
     458             :       // event handler, the render editable will use the outdated text stored in
     459             :       // the _plainText when handling the pointer event.
     460             :       //
     461             :       // If this happens, we need to make sure the new selection is still valid.
     462          12 :       final textLength = textSelectionDelegate.textEditingValue.text.length;
     463           3 :       nextSelection = nextSelection.copyWith(
     464           6 :         baseOffset: math.min(nextSelection.baseOffset, textLength),
     465           6 :         extentOffset: math.min(nextSelection.extentOffset, textLength),
     466             :       );
     467             :     }
     468           3 :     _handleSelectionChange(nextSelection, cause);
     469           3 :     _setTextEditingValue(
     470           9 :       textSelectionDelegate.textEditingValue.copyWith(selection: nextSelection),
     471             :       cause,
     472             :     );
     473             :   }
     474             : 
     475           3 :   void _handleSelectionChange(
     476             :     TextSelection nextSelection,
     477             :     SelectionChangedCause cause,
     478             :   ) {
     479             :     // Changes made by the keyboard can sometimes be "out of band" for listening
     480             :     // components, so always send those events, even if we didn't think it
     481             :     // changed. Also, focusing an empty field is sent as a selection change even
     482             :     // if the selection offset didn't change.
     483           6 :     final focusingEmpty = (nextSelection.baseOffset == 0) &&
     484           6 :         (nextSelection.extentOffset == 0) &&
     485           3 :         !hasFocus;
     486           6 :     if (nextSelection == selection &&
     487           3 :         cause != SelectionChangedCause.keyboard &&
     488             :         !focusingEmpty) {
     489             :       return;
     490             :     }
     491           4 :     onSelectionChanged?.call(nextSelection, this, cause);
     492             :   }
     493             : 
     494           6 :   static final Set<LogicalKeyboardKey> _movementKeys = <LogicalKeyboardKey>{
     495           3 :     LogicalKeyboardKey.arrowRight,
     496           3 :     LogicalKeyboardKey.arrowLeft,
     497           3 :     LogicalKeyboardKey.arrowUp,
     498           3 :     LogicalKeyboardKey.arrowDown,
     499             :   };
     500             : 
     501           6 :   static final Set<LogicalKeyboardKey> _shortcutKeys = <LogicalKeyboardKey>{
     502           3 :     LogicalKeyboardKey.keyA,
     503           3 :     LogicalKeyboardKey.keyC,
     504           3 :     LogicalKeyboardKey.keyV,
     505           3 :     LogicalKeyboardKey.keyX,
     506           3 :     LogicalKeyboardKey.delete,
     507           3 :     LogicalKeyboardKey.backspace,
     508             :   };
     509             : 
     510           9 :   static final Set<LogicalKeyboardKey> _nonModifierKeys = <LogicalKeyboardKey>{
     511           3 :     ..._shortcutKeys,
     512           3 :     ..._movementKeys,
     513             :   };
     514             : 
     515           4 :   static final Set<LogicalKeyboardKey> _modifierKeys = <LogicalKeyboardKey>{
     516           2 :     LogicalKeyboardKey.shift,
     517           2 :     LogicalKeyboardKey.control,
     518           2 :     LogicalKeyboardKey.alt,
     519             :   };
     520             : 
     521           4 :   static final Set<LogicalKeyboardKey> _macOsModifierKeys =
     522             :       <LogicalKeyboardKey>{
     523           2 :     LogicalKeyboardKey.shift,
     524           2 :     LogicalKeyboardKey.meta,
     525           2 :     LogicalKeyboardKey.alt,
     526             :   };
     527             : 
     528           6 :   static final Set<LogicalKeyboardKey> _interestingKeys = <LogicalKeyboardKey>{
     529           2 :     ..._modifierKeys,
     530           2 :     ..._macOsModifierKeys,
     531           2 :     ..._nonModifierKeys,
     532             :   };
     533             : 
     534           3 :   void _handleKeyEvent(RawKeyEvent keyEvent) {
     535             :     if (kIsWeb) {
     536             :       // On web platform, we should ignore the key because it's processed already.
     537             :       return;
     538             :     }
     539             : 
     540           3 :     if (keyEvent is! RawKeyDownEvent) return;
     541             :     final keysPressed =
     542           9 :         LogicalKeyboardKey.collapseSynonyms(RawKeyboard.instance.keysPressed);
     543           3 :     final key = keyEvent.logicalKey;
     544             : 
     545           6 :     final isMacOS = keyEvent.data is RawKeyEventDataMacOs;
     546           6 :     if (!_nonModifierKeys.contains(key) ||
     547             :         keysPressed
     548           5 :                 .difference(isMacOS ? _macOsModifierKeys : _modifierKeys)
     549           4 :                 .length >
     550             :             1 ||
     551           6 :         keysPressed.difference(_interestingKeys).isNotEmpty) {
     552             :       // If the most recently pressed key isn't a non-modifier key, or more than
     553             :       // one non-modifier key is down, or keys other than the ones we're interested in
     554             :       // are pressed, just ignore the keypress.
     555             :       return;
     556             :     }
     557             : 
     558             :     // TODO(ianh): It seems to be entirely possible for the selection to be null here, but
     559             :     // all the keyboard handling functions assume it is not.
     560           2 :     assert(selection != null);
     561             : 
     562             :     final isShortcutModifierPressed =
     563           3 :         isMacOS ? keyEvent.isMetaPressed : keyEvent.isControlPressed;
     564           4 :     if (isShortcutModifierPressed && _shortcutKeys.contains(key)) {
     565             :       // _handleShortcuts depends on being started in the same stack invocation
     566             :       // as the _handleKeyEvent method
     567           2 :       _handleShortcuts(key);
     568           2 :     } else if (key == LogicalKeyboardKey.delete) {
     569           2 :       _handleDelete(forward: true);
     570           1 :     } else if (key == LogicalKeyboardKey.backspace) {
     571           1 :       _handleDelete(forward: false);
     572             :     }
     573             :   }
     574             : 
     575             :   /// Returns the index into the string of the next character boundary after the
     576             :   /// given index.
     577             :   ///
     578             :   /// The character boundary is determined by the characters package, so
     579             :   /// surrogate pairs and extended grapheme clusters are considered.
     580             :   ///
     581             :   /// The index must be between 0 and string.length, inclusive. If given
     582             :   /// string.length, string.length is returned.
     583             :   ///
     584             :   /// Setting includeWhitespace to false will only return the index of non-space
     585             :   /// characters.
     586           2 :   @visibleForTesting
     587             :   static int nextCharacter(int index, String string,
     588             :       [bool includeWhitespace = true]) {
     589           7 :     assert(index >= 0 && index <= string.length);
     590           4 :     if (index == string.length) {
     591           1 :       return string.length;
     592             :     }
     593             : 
     594             :     var count = 0;
     595           6 :     final remaining = string.characters.skipWhile((String currentString) {
     596           2 :       if (count <= index) {
     597           4 :         count += currentString.length;
     598             :         return true;
     599             :       }
     600             :       if (includeWhitespace) {
     601             :         return false;
     602             :       }
     603           2 :       return _isWhitespace(currentString.codeUnitAt(0));
     604             :     });
     605           8 :     return string.length - remaining.toString().length;
     606             :   }
     607             : 
     608             :   /// Returns the index into the string of the previous character boundary
     609             :   /// before the given index.
     610             :   ///
     611             :   /// The character boundary is determined by the characters package, so
     612             :   /// surrogate pairs and extended grapheme clusters are considered.
     613             :   ///
     614             :   /// The index must be between 0 and string.length, inclusive. If index is 0,
     615             :   /// 0 will be returned.
     616             :   ///
     617             :   /// Setting includeWhitespace to false will only return the index of non-space
     618             :   /// characters.
     619           1 :   @visibleForTesting
     620             :   static int previousCharacter(int index, String string,
     621             :       [bool includeWhitespace = true]) {
     622           4 :     assert(index >= 0 && index <= string.length);
     623           1 :     if (index == 0) {
     624             :       return 0;
     625             :     }
     626             : 
     627             :     var count = 0;
     628             :     int? lastNonWhitespace;
     629           2 :     for (final currentString in string.characters) {
     630             :       if (!includeWhitespace &&
     631           1 :           !_isWhitespace(
     632           4 :               currentString.characters.first.toString().codeUnitAt(0))) {
     633             :         lastNonWhitespace = count;
     634             :       }
     635           3 :       if (count + currentString.length >= index) {
     636             :         return includeWhitespace ? count : lastNonWhitespace ?? 0;
     637             :       }
     638           2 :       count += currentString.length;
     639             :     }
     640             :     return 0;
     641             :   }
     642             : 
     643             :   // Return a new selection that has been moved up once.
     644             :   //
     645             :   // If it can't be moved up, the original TextSelection is returned.
     646           1 :   static TextSelection _moveGivenSelectionUp(
     647             :       TextSelection selection, String text) {
     648             :     // If the selection is already all the way up, there is nothing to do.
     649           3 :     if (selection.isCollapsed && selection.extentOffset <= 0) {
     650             :       return selection;
     651             :     }
     652             : 
     653             :     int previousExtent;
     654           3 :     if (selection.start != selection.end) {
     655           1 :       previousExtent = selection.start;
     656             :     } else {
     657           2 :       previousExtent = previousCharacter(selection.extentOffset, text);
     658             :     }
     659           1 :     final newSelection = selection.copyWith(
     660             :       extentOffset: previousExtent,
     661             :     );
     662             : 
     663           1 :     final newOffset = newSelection.extentOffset;
     664           2 :     return TextSelection.fromPosition(TextPosition(offset: newOffset));
     665             :   }
     666             : 
     667             :   // Return a new selection that has been moved down once.
     668             :   //
     669             :   // If it can't be moved down, the original TextSelection is returned.
     670           1 :   static TextSelection _moveGivenSelectionDown(
     671             :       TextSelection selection, String text) {
     672             :     // If the selection is already all the way down, there is nothing to do.
     673           4 :     if (selection.isCollapsed && selection.extentOffset >= text.length) {
     674             :       return selection;
     675             :     }
     676             : 
     677             :     int nextExtent;
     678           3 :     if (selection.start != selection.end) {
     679           1 :       nextExtent = selection.end;
     680             :     } else {
     681           2 :       nextExtent = nextCharacter(selection.extentOffset, text);
     682             :     }
     683           1 :     final nextSelection = selection.copyWith(extentOffset: nextExtent);
     684             : 
     685           1 :     var newOffset = nextSelection.extentOffset;
     686           3 :     newOffset = nextSelection.baseOffset > nextSelection.extentOffset
     687           0 :         ? nextSelection.baseOffset
     688           1 :         : nextSelection.extentOffset;
     689           2 :     return TextSelection.fromPosition(TextPosition(offset: newOffset));
     690             :   }
     691             : 
     692             :   // Return the offset at the start of the nearest word above the given
     693             :   // offset.
     694           1 :   static int _getAboveByWord(MongolTextPainter textPainter, int offset,
     695             :       [bool includeWhitespace = true]) {
     696             :     // If the offset is already all the way at the top, there is nothing to do.
     697           1 :     if (offset <= 0) {
     698             :       return offset;
     699             :     }
     700             : 
     701             :     // If we can just return the start of the text without checking for a word.
     702           1 :     if (offset == 1) {
     703             :       return 0;
     704             :     }
     705             : 
     706           2 :     final text = textPainter.text!.toPlainText();
     707           1 :     final startPoint = previousCharacter(offset, text, includeWhitespace);
     708           2 :     final word = textPainter.getWordBoundary(TextPosition(offset: startPoint));
     709           1 :     return word.start;
     710             :   }
     711             : 
     712             :   // Return the offset at the end of the nearest word below the given
     713             :   // offset.
     714           1 :   static int _getBelowByWord(MongolTextPainter textPainter, int offset,
     715             :       [bool includeWhitespace = true]) {
     716             :     // If the selection is already all the way at the bottom, there is nothing to do.
     717           2 :     final text = textPainter.text!.toPlainText();
     718           2 :     if (offset == text.length) {
     719             :       return offset;
     720             :     }
     721             : 
     722             :     // If we can just return the end of the text without checking for a word.
     723           5 :     if (offset == text.length - 1 || offset == text.length) {
     724           0 :       return text.length;
     725             :     }
     726             : 
     727             :     final startPoint =
     728           2 :         includeWhitespace || !_isWhitespace(text.codeUnitAt(offset))
     729             :             ? offset
     730           1 :             : nextCharacter(offset, text, includeWhitespace);
     731             :     final nextWord =
     732           2 :         textPainter.getWordBoundary(TextPosition(offset: startPoint));
     733           1 :     return nextWord.end;
     734             :   }
     735             : 
     736             :   // Return the given TextSelection extended up to the beginning of the
     737             :   // nearest word.
     738             :   //
     739             :   // See extendSelectionUpByWord for a detailed explanation of the two
     740             :   // optional parameters.
     741           0 :   static TextSelection _extendGivenSelectionUpByWord(
     742             :     MongolTextPainter textPainter,
     743             :     TextSelection selection, [
     744             :     bool includeWhitespace = true,
     745             :     bool stopAtReversal = false,
     746             :   ]) {
     747             :     // If the selection is already all the way at the top, there is nothing to do.
     748           0 :     if (selection.isCollapsed && selection.extentOffset <= 0) {
     749             :       return selection;
     750             :     }
     751             : 
     752             :     final leftOffset =
     753           0 :         _getAboveByWord(textPainter, selection.extentOffset, includeWhitespace);
     754             : 
     755             :     if (stopAtReversal &&
     756           0 :         selection.extentOffset > selection.baseOffset &&
     757           0 :         leftOffset < selection.baseOffset) {
     758           0 :       return selection.copyWith(
     759           0 :         extentOffset: selection.baseOffset,
     760             :       );
     761             :     }
     762             : 
     763           0 :     return selection.copyWith(
     764             :       extentOffset: leftOffset,
     765             :     );
     766             :   }
     767             : 
     768             :   // Return the given TextSelection extended down to the end of the nearest
     769             :   // word.
     770             :   //
     771             :   // See extendSelectionDownByWord for a detailed explanation of the two
     772             :   // optional parameters.
     773           0 :   static TextSelection _extendGivenSelectionDownByWord(
     774             :     MongolTextPainter textPainter,
     775             :     TextSelection selection, [
     776             :     bool includeWhitespace = true,
     777             :     bool stopAtReversal = false,
     778             :   ]) {
     779             :     // If the selection is already all the way down, there is nothing to do.
     780           0 :     final text = textPainter.text!.toPlainText();
     781           0 :     if (selection.isCollapsed && selection.extentOffset == text.length) {
     782             :       return selection;
     783             :     }
     784             : 
     785             :     final rightOffset =
     786           0 :         _getBelowByWord(textPainter, selection.extentOffset, includeWhitespace);
     787             : 
     788             :     if (stopAtReversal &&
     789           0 :         selection.baseOffset > selection.extentOffset &&
     790           0 :         rightOffset > selection.baseOffset) {
     791           0 :       return selection.copyWith(
     792           0 :         extentOffset: selection.baseOffset,
     793             :       );
     794             :     }
     795             : 
     796           0 :     return selection.copyWith(
     797             :       extentOffset: rightOffset,
     798             :     );
     799             :   }
     800             : 
     801             :   // Return the given TextSelection moved up to the end of the nearest word.
     802             :   //
     803             :   // A TextSelection that isn't collapsed will be collapsed and moved from the
     804             :   // extentOffset.
     805           1 :   static TextSelection _moveGivenSelectionUpByWord(
     806             :     MongolTextPainter textPainter,
     807             :     TextSelection selection, [
     808             :     bool includeWhitespace = true,
     809             :   ]) {
     810             :     // If the selection is already all the way at the top, there is nothing to do.
     811           3 :     if (selection.isCollapsed && selection.extentOffset <= 0) {
     812             :       return selection;
     813             :     }
     814             : 
     815             :     final leftOffset =
     816           2 :         _getAboveByWord(textPainter, selection.extentOffset, includeWhitespace);
     817           1 :     return selection.copyWith(
     818             :       baseOffset: leftOffset,
     819             :       extentOffset: leftOffset,
     820             :     );
     821             :   }
     822             : 
     823             :   // Return the given TextSelection moved down to the end of the nearest word.
     824             :   //
     825             :   // A TextSelection that isn't collapsed will be collapsed and moved from the
     826             :   // extentOffset.
     827           1 :   static TextSelection _moveGivenSelectionDownByWord(
     828             :     MongolTextPainter textPainter,
     829             :     TextSelection selection, [
     830             :     bool includeWhitespace = true,
     831             :   ]) {
     832             :     // If the selection is already all the way at the bottom, there is nothing to do.
     833           2 :     final text = textPainter.text!.toPlainText();
     834           4 :     if (selection.isCollapsed && selection.extentOffset == text.length) {
     835             :       return selection;
     836             :     }
     837             : 
     838             :     final rightOffset =
     839           2 :         _getBelowByWord(textPainter, selection.extentOffset, includeWhitespace);
     840           1 :     return selection.copyWith(
     841             :       baseOffset: rightOffset,
     842             :       extentOffset: rightOffset,
     843             :     );
     844             :   }
     845             : 
     846           1 :   static TextSelection _extendGivenSelectionUp(
     847             :     TextSelection selection,
     848             :     String text, [
     849             :     bool includeWhitespace = true,
     850             :   ]) {
     851             :     // If the selection is already all the way at the top, there is nothing to do.
     852           2 :     if (selection.extentOffset <= 0) {
     853             :       return selection;
     854             :     }
     855             :     final previousExtent =
     856           2 :         previousCharacter(selection.extentOffset, text, includeWhitespace);
     857           1 :     return selection.copyWith(extentOffset: previousExtent);
     858             :   }
     859             : 
     860           1 :   static TextSelection _extendGivenSelectionDown(
     861             :     TextSelection selection,
     862             :     String text, [
     863             :     bool includeWhitespace = true,
     864             :   ]) {
     865             :     // If the selection is already all the way at the bottom, there is nothing to do.
     866           3 :     if (selection.extentOffset >= text.length) {
     867             :       return selection;
     868             :     }
     869             :     final nextExtent =
     870           2 :         nextCharacter(selection.extentOffset, text, includeWhitespace);
     871           1 :     return selection.copyWith(extentOffset: nextExtent);
     872             :   }
     873             : 
     874             :   // Extend the current selection to the end of the field.
     875             :   //
     876             :   // If selectionEnabled is false, keeps the selection collapsed and moves it to
     877             :   // the end.
     878             :   //
     879             :   // The given [SelectionChangedCause] indicates the cause of this change and
     880             :   // will be passed to [onSelectionChanged].
     881             :   //
     882             :   // See also:
     883             :   //
     884             :   //   * _extendSelectionToStart
     885           0 :   void _extendSelectionToEnd(SelectionChangedCause cause) {
     886           0 :     if (selection!.extentOffset == _plainText.length) {
     887             :       return;
     888             :     }
     889           0 :     if (!selectionEnabled) {
     890           0 :       return moveSelectionToEnd(cause);
     891             :     }
     892             : 
     893           0 :     final nextSelection = selection!.copyWith(
     894           0 :       extentOffset: _plainText.length,
     895             :     );
     896           0 :     _setSelection(nextSelection, cause);
     897             :   }
     898             : 
     899             :   // Extend the current selection to the start of the field.
     900             :   //
     901             :   // If selectionEnabled is false, keeps the selection collapsed and moves it to
     902             :   // the start.
     903             :   //
     904             :   // The given [SelectionChangedCause] indicates the cause of this change and
     905             :   // will be passed to [onSelectionChanged].
     906             :   //
     907             :   // See also:
     908             :   //
     909             :   //   * _expandSelectionToEnd
     910           0 :   void _extendSelectionToStart(SelectionChangedCause cause) {
     911           0 :     if (selection!.extentOffset == 0) {
     912             :       return;
     913             :     }
     914           0 :     if (!selectionEnabled) {
     915           0 :       return moveSelectionToStart(cause);
     916             :     }
     917             : 
     918           0 :     final nextSelection = selection!.copyWith(
     919             :       extentOffset: 0,
     920             :     );
     921           0 :     _setSelection(nextSelection, cause);
     922             :   }
     923             : 
     924             :   // Returns the TextPosition to the left or right of the given offset.
     925           0 :   TextPosition _getTextPositionHorizontal(
     926             :       int textOffset, double horizontalOffset) {
     927           0 :     final caretOffset = _textPainter.getOffsetForCaret(
     928           0 :         TextPosition(offset: textOffset), _caretPrototype);
     929           0 :     final caretOffsetTranslated = caretOffset.translate(horizontalOffset, 0.0);
     930           0 :     return _textPainter.getPositionForOffset(caretOffsetTranslated);
     931             :   }
     932             : 
     933             :   // Returns the TextPosition left of the given offset into _plainText.
     934             :   //
     935             :   // If the offset is already on the first line, the given offset will be
     936             :   // returned.
     937           0 :   TextPosition _getTextPositionLeft(int offset) {
     938             :     // The caret offset gives a location in the upper left hand corner of
     939             :     // the caret so the middle of the line to the left is a half line to the left of that
     940             :     // point and the line to the right is 1.5 lines to the right of that point.
     941           0 :     final preferredLineWidth = _textPainter.preferredLineWidth;
     942           0 :     final horizontalOffset = -0.5 * preferredLineWidth;
     943           0 :     return _getTextPositionHorizontal(offset, horizontalOffset);
     944             :   }
     945             : 
     946             :   // Returns the TextPosition to the right of the given offset into _plainText.
     947             :   //
     948             :   // If the offset is already on the last line, the given offset will be
     949             :   // returned.
     950           0 :   TextPosition _getTextPositionRight(int offset) {
     951             :     // The caret offset gives a location in the upper left hand corner of
     952             :     // the caret so the middle of the line to the left is a half line to the left of that
     953             :     // point and the line to the right is 1.5 lines to the right of that point.
     954           0 :     final preferredLineWidth = _textPainter.preferredLineWidth;
     955           0 :     final horizontalOffset = 1.5 * preferredLineWidth;
     956           0 :     return _getTextPositionHorizontal(offset, horizontalOffset);
     957             :   }
     958             : 
     959             :   // Deletes the text within `selection` if it's non-empty.
     960           0 :   void _deleteSelection(TextSelection selection, SelectionChangedCause cause) {
     961           0 :     assert(!selection.isCollapsed);
     962             : 
     963           0 :     if (_readOnly || !selection.isValid || selection.isCollapsed) {
     964             :       return;
     965             :     }
     966             : 
     967           0 :     final text = textSelectionDelegate.textEditingValue.text;
     968           0 :     final textBefore = selection.textBefore(text);
     969           0 :     final textAfter = selection.textAfter(text);
     970           0 :     final cursorPosition = math.min(selection.start, selection.end);
     971           0 :     final newSelection = TextSelection.collapsed(offset: cursorPosition);
     972           0 :     _setTextEditingValue(
     973           0 :       TextEditingValue(text: textBefore + textAfter, selection: newSelection),
     974             :       cause,
     975             :     );
     976             :   }
     977             : 
     978             :   // Deletes the current non-empty selection.
     979             :   //
     980             :   // Operates on the text/selection contained in textSelectionDelegate, and does
     981             :   // not depend on `MongolRenderEditable.selection`.
     982             :   //
     983             :   // If the selection is currently non-empty, this method deletes the selected
     984             :   // text and returns true. Otherwise this method does nothing and returns
     985             :   // false.
     986           1 :   bool _deleteNonEmptySelection(SelectionChangedCause cause) {
     987             :     // TODO(LongCatIsLooong): remove this method from `RenderEditable`
     988             :     // https://github.com/flutter/flutter/issues/80226.
     989           1 :     assert(!readOnly);
     990           2 :     final controllerValue = textSelectionDelegate.textEditingValue;
     991           1 :     final selection = controllerValue.selection;
     992           1 :     assert(selection.isValid);
     993             : 
     994           1 :     if (selection.isCollapsed) {
     995             :       return false;
     996             :     }
     997             : 
     998           2 :     final textBefore = selection.textBefore(controllerValue.text);
     999           2 :     final textAfter = selection.textAfter(controllerValue.text);
    1000           2 :     final newSelection = TextSelection.collapsed(offset: selection.start);
    1001           1 :     final composing = controllerValue.composing;
    1002           1 :     final newComposingRange = !composing.isValid || composing.isCollapsed
    1003             :         ? TextRange.empty
    1004           0 :         : TextRange(
    1005           0 :             start: composing.start -
    1006           0 :                 (composing.start - selection.start)
    1007           0 :                     .clamp(0, selection.end - selection.start),
    1008           0 :             end: composing.end -
    1009           0 :                 (composing.end - selection.start)
    1010           0 :                     .clamp(0, selection.end - selection.start),
    1011             :           );
    1012             : 
    1013           1 :     _setTextEditingValue(
    1014           1 :       TextEditingValue(
    1015           1 :         text: textBefore + textAfter,
    1016             :         selection: newSelection,
    1017             :         composing: newComposingRange,
    1018             :       ),
    1019             :       cause,
    1020             :     );
    1021             :     return true;
    1022             :   }
    1023             : 
    1024             :   // Deletes the from the current collapsed selection to the start of the field.
    1025             :   //
    1026             :   // The given SelectionChangedCause indicates the cause of this change and
    1027             :   // will be passed to onSelectionChanged.
    1028             :   //
    1029             :   // See also:
    1030             :   //   * _deleteToEnd
    1031           1 :   void _deleteToStart(TextSelection selection, SelectionChangedCause cause) {
    1032           1 :     assert(selection.isCollapsed);
    1033             : 
    1034           2 :     if (_readOnly || !selection.isValid) {
    1035             :       return;
    1036             :     }
    1037             : 
    1038           3 :     final text = textSelectionDelegate.textEditingValue.text;
    1039           1 :     final textBefore = selection.textBefore(text);
    1040             : 
    1041           1 :     if (textBefore.isEmpty) {
    1042             :       return;
    1043             :     }
    1044             : 
    1045           1 :     final textAfter = selection.textAfter(text);
    1046             :     const newSelection = TextSelection.collapsed(offset: 0);
    1047           1 :     _setTextEditingValue(
    1048           1 :       TextEditingValue(text: textAfter, selection: newSelection),
    1049             :       cause,
    1050             :     );
    1051             :   }
    1052             : 
    1053             :   // Deletes the from the current collapsed selection to the end of the field.
    1054             :   //
    1055             :   // The given SelectionChangedCause indicates the cause of this change and
    1056             :   // will be passed to onSelectionChanged.
    1057             :   //
    1058             :   // See also:
    1059             :   //   * _deleteToStart
    1060           1 :   void _deleteToEnd(TextSelection selection, SelectionChangedCause cause) {
    1061           1 :     assert(selection.isCollapsed);
    1062             : 
    1063           2 :     if (_readOnly || !selection.isValid) {
    1064             :       return;
    1065             :     }
    1066             : 
    1067           3 :     final text = textSelectionDelegate.textEditingValue.text;
    1068           1 :     final textAfter = selection.textAfter(text);
    1069             : 
    1070           1 :     if (textAfter.isEmpty) {
    1071             :       return;
    1072             :     }
    1073             : 
    1074           1 :     final textBefore = selection.textBefore(text);
    1075           2 :     final newSelection = TextSelection.collapsed(offset: textBefore.length);
    1076           1 :     _setTextEditingValue(
    1077           1 :       TextEditingValue(text: textBefore, selection: newSelection),
    1078             :       cause,
    1079             :     );
    1080             :   }
    1081             : 
    1082             :   /// Deletes backwards from the selection in [textSelectionDelegate].
    1083             :   ///
    1084             :   /// This method operates on the text/selection contained in
    1085             :   /// [textSelectionDelegate], and does not depend on [selection].
    1086             :   ///
    1087             :   /// If the selection is collapsed, deletes a single character before the
    1088             :   /// cursor.
    1089             :   ///
    1090             :   /// If the selection is not collapsed, deletes the selection.
    1091             :   ///
    1092             :   /// The given [SelectionChangedCause] indicates the cause of this change and
    1093             :   /// will be passed to [onSelectionChanged].
    1094             :   ///
    1095             :   /// See also:
    1096             :   ///
    1097             :   ///   * [deleteForward], which is same but in the opposite direction.
    1098           1 :   void delete(SelectionChangedCause cause) {
    1099             :     // `delete` does not depend on the text layout, and the boundary analysis is
    1100             :     // done using the `previousCharacter` method instead of ICU, we can keep
    1101             :     // deleting without having to layout the text. For this reason, we can
    1102             :     // directly delete the character before the caret in the controller.
    1103             :     //
    1104             :     // TODO(LongCatIsLooong): remove this method from RenderEditable.
    1105             :     // https://github.com/flutter/flutter/issues/80226.
    1106           2 :     final controllerValue = textSelectionDelegate.textEditingValue;
    1107           1 :     final selection = controllerValue.selection;
    1108             : 
    1109           3 :     if (!selection.isValid || readOnly || _deleteNonEmptySelection(cause)) {
    1110             :       return;
    1111             :     }
    1112             : 
    1113           1 :     assert(selection.isCollapsed);
    1114           2 :     final textBefore = selection.textBefore(controllerValue.text);
    1115           1 :     if (textBefore.isEmpty) {
    1116             :       return;
    1117             :     }
    1118             : 
    1119           2 :     final textAfter = selection.textAfter(controllerValue.text);
    1120             : 
    1121           2 :     final characterBoundary = previousCharacter(textBefore.length, textBefore);
    1122           1 :     final newSelection = TextSelection.collapsed(offset: characterBoundary);
    1123           1 :     final composing = controllerValue.composing;
    1124           2 :     assert(textBefore.length >= characterBoundary);
    1125           2 :     final newComposingRange = !composing.isValid || composing.isCollapsed
    1126             :         ? TextRange.empty
    1127           1 :         : TextRange(
    1128           2 :             start: composing.start -
    1129           2 :                 (composing.start - characterBoundary)
    1130           3 :                     .clamp(0, textBefore.length - characterBoundary),
    1131           2 :             end: composing.end -
    1132           2 :                 (composing.end - characterBoundary)
    1133           3 :                     .clamp(0, textBefore.length - characterBoundary),
    1134             :           );
    1135             : 
    1136           1 :     _setTextEditingValue(
    1137           1 :       TextEditingValue(
    1138           2 :         text: textBefore.substring(0, characterBoundary) + textAfter,
    1139             :         selection: newSelection,
    1140             :         composing: newComposingRange,
    1141             :       ),
    1142             :       cause,
    1143             :     );
    1144             :   }
    1145             : 
    1146             :   /// Deletes a word backwards from the current selection.
    1147             :   ///
    1148             :   /// If the [selection] is collapsed, deletes a word before the cursor.
    1149             :   ///
    1150             :   /// If the [selection] is not collapsed, deletes the selection.
    1151             :   ///
    1152             :   /// If [obscureText] is true, it treats the whole text content as
    1153             :   /// a single word.
    1154             :   ///
    1155             :   /// By default, includeWhitespace is set to true, meaning that whitespace can
    1156             :   /// be considered a word in itself.  If set to false, the selection will be
    1157             :   /// extended past any whitespace and the first word following the whitespace.
    1158             :   ///
    1159             :   /// See also:
    1160             :   ///
    1161             :   ///   * [deleteForwardByWord], which is same but in the opposite direction.
    1162           1 :   void deleteByWord(SelectionChangedCause cause,
    1163             :       [bool includeWhitespace = true]) {
    1164           1 :     assert(_selection != null);
    1165             : 
    1166           3 :     if (_readOnly || !_selection!.isValid) {
    1167             :       return;
    1168             :     }
    1169             : 
    1170           2 :     if (!_selection!.isCollapsed) {
    1171           0 :       return _deleteSelection(_selection!, cause);
    1172             :     }
    1173             : 
    1174             :     // When the text is obscured, the whole thing is treated as one big line.
    1175           1 :     if (obscureText) {
    1176           2 :       return _deleteToStart(_selection!, cause);
    1177             :     }
    1178             : 
    1179           3 :     final text = textSelectionDelegate.textEditingValue.text;
    1180           2 :     var textBefore = _selection!.textBefore(text);
    1181           1 :     if (textBefore.isEmpty) {
    1182             :       return;
    1183             :     }
    1184             : 
    1185             :     final characterBoundary =
    1186           3 :         _getAboveByWord(_textPainter, textBefore.length, includeWhitespace);
    1187           2 :     textBefore = textBefore.trimRight().substring(0, characterBoundary);
    1188             : 
    1189           2 :     final textAfter = _selection!.textAfter(text);
    1190           1 :     final newSelection = TextSelection.collapsed(offset: characterBoundary);
    1191           1 :     _setTextEditingValue(
    1192           2 :       TextEditingValue(text: textBefore + textAfter, selection: newSelection),
    1193             :       cause,
    1194             :     );
    1195             :   }
    1196             : 
    1197             :   /// Deletes a line backwards from the current selection.
    1198             :   ///
    1199             :   /// If the [selection] is collapsed, deletes a line before the cursor.
    1200             :   ///
    1201             :   /// If the [selection] is not collapsed, deletes the selection.
    1202             :   ///
    1203             :   /// If [obscureText] is true, it treats the whole text content as
    1204             :   /// a single word.
    1205             :   ///
    1206             :   /// See also:
    1207             :   ///
    1208             :   ///   * [deleteForwardByLine], which is same but in the opposite direction.
    1209           1 :   void deleteByLine(SelectionChangedCause cause) {
    1210           1 :     assert(_selection != null);
    1211             : 
    1212           3 :     if (_readOnly || !_selection!.isValid) {
    1213             :       return;
    1214             :     }
    1215             : 
    1216           2 :     if (!_selection!.isCollapsed) {
    1217           0 :       return _deleteSelection(_selection!, cause);
    1218             :     }
    1219             : 
    1220             :     // When the text is obscured, the whole thing is treated as one big line.
    1221           1 :     if (obscureText) {
    1222           2 :       return _deleteToStart(_selection!, cause);
    1223             :     }
    1224             : 
    1225           3 :     final text = textSelectionDelegate.textEditingValue.text;
    1226           2 :     var textBefore = _selection!.textBefore(text);
    1227           1 :     if (textBefore.isEmpty) {
    1228             :       return;
    1229             :     }
    1230             : 
    1231             :     // When there is a line break, line delete shouldn't do anything
    1232             :     final isPreviousCharacterBreakLine =
    1233           4 :         textBefore.codeUnitAt(textBefore.length - 1) == 0x0A;
    1234             :     if (isPreviousCharacterBreakLine) {
    1235             :       return;
    1236             :     }
    1237             : 
    1238           4 :     final line = _getLineAtOffset(TextPosition(offset: textBefore.length - 1));
    1239           2 :     textBefore = textBefore.substring(0, line.start);
    1240             : 
    1241           2 :     final textAfter = _selection!.textAfter(text);
    1242           2 :     final newSelection = TextSelection.collapsed(offset: textBefore.length);
    1243           1 :     _setTextEditingValue(
    1244           2 :       TextEditingValue(text: textBefore + textAfter, selection: newSelection),
    1245             :       cause,
    1246             :     );
    1247             :   }
    1248             : 
    1249             :   /// Deletes in the foward direction, from the current selection in
    1250             :   /// [textSelectionDelegate].
    1251             :   ///
    1252             :   /// This method operates on the text/selection contained in
    1253             :   /// [textSelectionDelegate], and does not depend on [selection].
    1254             :   ///
    1255             :   /// If the selection is collapsed, deletes a single character after the
    1256             :   /// cursor.
    1257             :   ///
    1258             :   /// If the selection is not collapsed, deletes the selection.
    1259             :   ///
    1260             :   /// See also:
    1261             :   ///
    1262             :   ///   * [delete], which is same but in the opposite direction.
    1263           1 :   void deleteForward(SelectionChangedCause cause) {
    1264             :     // TODO(LongCatIsLooong): remove this method from RenderEditable.
    1265             :     // https://github.com/flutter/flutter/issues/80226.
    1266           2 :     final controllerValue = textSelectionDelegate.textEditingValue;
    1267           1 :     final selection = controllerValue.selection;
    1268             : 
    1269           3 :     if (!selection.isValid || _readOnly || _deleteNonEmptySelection(cause)) {
    1270             :       return;
    1271             :     }
    1272             : 
    1273           1 :     assert(selection.isCollapsed);
    1274           2 :     final textAfter = selection.textAfter(controllerValue.text);
    1275           1 :     if (textAfter.isEmpty) {
    1276             :       return;
    1277             :     }
    1278             : 
    1279           2 :     final textBefore = selection.textBefore(controllerValue.text);
    1280           1 :     final characterBoundary = nextCharacter(0, textAfter);
    1281           1 :     final composing = controllerValue.composing;
    1282           2 :     final newComposingRange = !composing.isValid || composing.isCollapsed
    1283             :         ? TextRange.empty
    1284           1 :         : TextRange(
    1285           2 :             start: composing.start -
    1286           3 :                 (composing.start - textBefore.length)
    1287           1 :                     .clamp(0, characterBoundary),
    1288           2 :             end: composing.end -
    1289           4 :                 (composing.end - textBefore.length).clamp(0, characterBoundary),
    1290             :           );
    1291           1 :     _setTextEditingValue(
    1292           1 :       TextEditingValue(
    1293           2 :         text: textBefore + textAfter.substring(characterBoundary),
    1294             :         selection: selection,
    1295             :         composing: newComposingRange,
    1296             :       ),
    1297             :       cause,
    1298             :     );
    1299             :   }
    1300             : 
    1301             :   /// Deletes a word in the foward direction from the current selection.
    1302             :   ///
    1303             :   /// If the [selection] is collapsed, deletes a word after the cursor.
    1304             :   ///
    1305             :   /// If the [selection] is not collapsed, deletes the selection.
    1306             :   ///
    1307             :   /// If [obscureText] is true, it treats the whole text content as
    1308             :   /// a single word.
    1309             :   ///
    1310             :   /// See also:
    1311             :   ///
    1312             :   ///   * [deleteByWord], which is same but in the opposite direction.
    1313           1 :   void deleteForwardByWord(SelectionChangedCause cause,
    1314             :       [bool includeWhitespace = true]) {
    1315           1 :     assert(_selection != null);
    1316             : 
    1317           3 :     if (_readOnly || !_selection!.isValid) {
    1318             :       return;
    1319             :     }
    1320             : 
    1321           2 :     if (!_selection!.isCollapsed) {
    1322           0 :       return _deleteSelection(_selection!, cause);
    1323             :     }
    1324             : 
    1325             :     // When the text is obscured, the whole thing is treated as one big word.
    1326           1 :     if (obscureText) {
    1327           2 :       return _deleteToEnd(_selection!, cause);
    1328             :     }
    1329             : 
    1330           3 :     final text = textSelectionDelegate.textEditingValue.text;
    1331           2 :     var textAfter = _selection!.textAfter(text);
    1332             : 
    1333           1 :     if (textAfter.isEmpty) {
    1334             :       return;
    1335             :     }
    1336             : 
    1337           2 :     final textBefore = _selection!.textBefore(text);
    1338             :     final characterBoundary =
    1339           3 :         _getBelowByWord(_textPainter, textBefore.length, includeWhitespace);
    1340           3 :     textAfter = textAfter.substring(characterBoundary - textBefore.length);
    1341             : 
    1342           1 :     _setTextEditingValue(
    1343           3 :       TextEditingValue(text: textBefore + textAfter, selection: _selection!),
    1344             :       cause,
    1345             :     );
    1346             :   }
    1347             : 
    1348             :   /// Deletes a line in the foward direction from the current selection.
    1349             :   ///
    1350             :   /// If the [selection] is collapsed, deletes a line after the cursor.
    1351             :   ///
    1352             :   /// If the [selection] is not collapsed, deletes the selection.
    1353             :   ///
    1354             :   /// If [obscureText] is true, it treats the whole text content as
    1355             :   /// a single word.
    1356             :   ///
    1357             :   /// See also:
    1358             :   ///
    1359             :   ///   * [deleteByLine], which is same but in the opposite direction.
    1360           1 :   void deleteForwardByLine(SelectionChangedCause cause) {
    1361           1 :     assert(_selection != null);
    1362             : 
    1363           3 :     if (_readOnly || !_selection!.isValid) {
    1364             :       return;
    1365             :     }
    1366             : 
    1367           2 :     if (!_selection!.isCollapsed) {
    1368           0 :       return _deleteSelection(_selection!, cause);
    1369             :     }
    1370             : 
    1371             :     // When the text is obscured, the whole thing is treated as one big line.
    1372           1 :     if (obscureText) {
    1373           2 :       return _deleteToEnd(_selection!, cause);
    1374             :     }
    1375             : 
    1376           3 :     final text = textSelectionDelegate.textEditingValue.text;
    1377           2 :     var textAfter = _selection!.textAfter(text);
    1378           1 :     if (textAfter.isEmpty) {
    1379             :       return;
    1380             :     }
    1381             : 
    1382             :     // When there is a line break, it shouldn't do anything.
    1383           2 :     final isNextCharacterBreakLine = textAfter.codeUnitAt(0) == 0x0A;
    1384             :     if (isNextCharacterBreakLine) {
    1385             :       return;
    1386             :     }
    1387             : 
    1388           2 :     final textBefore = _selection!.textBefore(text);
    1389           3 :     final line = _getLineAtOffset(TextPosition(offset: textBefore.length));
    1390             :     textAfter =
    1391           5 :         textAfter.substring(line.end - textBefore.length, textAfter.length);
    1392             : 
    1393           1 :     _setTextEditingValue(
    1394           3 :       TextEditingValue(text: textBefore + textAfter, selection: _selection!),
    1395             :       cause,
    1396             :     );
    1397             :   }
    1398             : 
    1399             :   /// Keeping [selection]'s [TextSelection.baseOffset] fixed, move the
    1400             :   /// [TextSelection.extentOffset] right by one line.
    1401             :   ///
    1402             :   /// If [selectionEnabled] is false, keeps the selection collapsed and just
    1403             :   /// moves it right.
    1404             :   ///
    1405             :   /// The given [SelectionChangedCause] indicates the cause of this change and
    1406             :   /// will be passed to [onSelectionChanged].
    1407             :   ///
    1408             :   /// See also:
    1409             :   ///
    1410             :   ///   * [extendSelectionUp], which is same but in the opposite direction.
    1411           0 :   void extendSelectionRight(SelectionChangedCause cause) {
    1412           0 :     assert(selection != null);
    1413             : 
    1414             :     // If the selection is collapsed at the end of the field already, then
    1415             :     // nothing happens.
    1416           0 :     if (selection!.isCollapsed &&
    1417           0 :         selection!.extentOffset >= _plainText.length) {
    1418             :       return;
    1419             :     }
    1420           0 :     if (!selectionEnabled) {
    1421           0 :       return moveSelectionRight(cause);
    1422             :     }
    1423             : 
    1424           0 :     final positionBelow = _getTextPositionRight(selection!.extentOffset);
    1425             :     late final TextSelection nextSelection;
    1426           0 :     if (positionBelow.offset == selection!.extentOffset) {
    1427           0 :       nextSelection = selection!.copyWith(
    1428           0 :         extentOffset: _plainText.length,
    1429             :       );
    1430           0 :       _wasSelectingHorizontallyWithKeyboard = true;
    1431           0 :     } else if (_wasSelectingHorizontallyWithKeyboard) {
    1432           0 :       nextSelection = selection!.copyWith(
    1433           0 :         extentOffset: _cursorResetLocation,
    1434             :       );
    1435           0 :       _wasSelectingHorizontallyWithKeyboard = false;
    1436             :     } else {
    1437           0 :       nextSelection = selection!.copyWith(
    1438           0 :         extentOffset: positionBelow.offset,
    1439             :       );
    1440           0 :       _cursorResetLocation = nextSelection.extentOffset;
    1441             :     }
    1442             : 
    1443           0 :     _setSelection(nextSelection, cause);
    1444             :   }
    1445             : 
    1446             :   /// Expand the current [selection] to the end of the field.
    1447             :   ///
    1448             :   /// The selection will never shrink. The [TextSelection.extentOffset] will
    1449             :   // always be at the end of the field, regardless of the original order of
    1450             :   /// [TextSelection.baseOffset] and [TextSelection.extentOffset].
    1451             :   ///
    1452             :   /// If [selectionEnabled] is false, keeps the selection collapsed and moves it
    1453             :   /// to the end.
    1454             :   ///
    1455             :   /// See also:
    1456             :   ///
    1457             :   ///   * [expandSelectionToStart], which is same but in the opposite direction.
    1458           0 :   void expandSelectionToEnd(SelectionChangedCause cause) {
    1459           0 :     assert(selection != null);
    1460             : 
    1461           0 :     if (selection!.extentOffset == _plainText.length) {
    1462             :       return;
    1463             :     }
    1464           0 :     if (!selectionEnabled) {
    1465           0 :       return moveSelectionToEnd(cause);
    1466             :     }
    1467             : 
    1468           0 :     final firstOffset = math.max(
    1469             :         0,
    1470           0 :         math.min(
    1471           0 :           selection!.baseOffset,
    1472           0 :           selection!.extentOffset,
    1473             :         ));
    1474           0 :     final nextSelection = TextSelection(
    1475             :       baseOffset: firstOffset,
    1476           0 :       extentOffset: _plainText.length,
    1477             :     );
    1478           0 :     _setSelection(nextSelection, cause);
    1479             :   }
    1480             : 
    1481             :   /// Keeping [selection]'s [TextSelection.baseOffset] fixed, move the
    1482             :   /// [TextSelection.extentOffset] up.
    1483             :   ///
    1484             :   /// If [selectionEnabled] is false, keeps the selection collapsed and moves it
    1485             :   /// up.
    1486             :   ///
    1487             :   /// See also:
    1488             :   ///
    1489             :   ///   * [extendSelectionDown], which is same but in the opposite direction.
    1490           1 :   void extendSelectionUp(SelectionChangedCause cause) {
    1491           1 :     assert(selection != null);
    1492             : 
    1493           1 :     if (!selectionEnabled) {
    1494           0 :       return moveSelectionUp(cause);
    1495             :     }
    1496             : 
    1497           1 :     final nextSelection = _extendGivenSelectionUp(
    1498           1 :       selection!,
    1499           1 :       _plainText,
    1500             :     );
    1501           2 :     if (nextSelection == selection) {
    1502             :       return;
    1503             :     }
    1504           4 :     final distance = selection!.extentOffset - nextSelection.extentOffset;
    1505           2 :     _cursorResetLocation -= distance;
    1506           1 :     _setSelection(nextSelection, cause);
    1507             :   }
    1508             : 
    1509             :   /// Extend the current [selection] to the start of
    1510             :   /// [TextSelection.extentOffset]'s line.
    1511             :   ///
    1512             :   /// Uses [TextSelection.baseOffset] as a pivot point and doesn't change it.
    1513             :   /// If [TextSelection.extentOffset] is below [TextSelection.baseOffset],
    1514             :   /// then collapses the selection.
    1515             :   ///
    1516             :   /// If [selectionEnabled] is false, keeps the selection collapsed and moves it
    1517             :   /// up by line.
    1518             :   ///
    1519             :   /// See also:
    1520             :   ///
    1521             :   ///   * [extendSelectionDownByLine], which is same but in the opposite
    1522             :   ///     direction.
    1523             :   ///   * [expandSelectionDownByLine], which strictly grows the selection
    1524             :   ///     regardless of the order.
    1525           0 :   void extendSelectionUpByLine(SelectionChangedCause cause) {
    1526           0 :     assert(selection != null);
    1527             : 
    1528           0 :     if (!selectionEnabled) {
    1529           0 :       return moveSelectionUpByLine(cause);
    1530             :     }
    1531             : 
    1532             :     // When going up, we want to skip over any whitespace before the line,
    1533             :     // so we go back to the first non-whitespace before asking for the line
    1534             :     // bounds, since _getLineAtOffset finds the line boundaries without
    1535             :     // including whitespace (like the newline).
    1536             :     final startPoint =
    1537           0 :         previousCharacter(selection!.extentOffset, _plainText, false);
    1538           0 :     final selectedLine = _getLineAtOffset(TextPosition(offset: startPoint));
    1539             : 
    1540             :     late final TextSelection nextSelection;
    1541           0 :     if (selection!.extentOffset > selection!.baseOffset) {
    1542           0 :       nextSelection = selection!.copyWith(
    1543           0 :         extentOffset: selection!.baseOffset,
    1544             :       );
    1545             :     } else {
    1546           0 :       nextSelection = selection!.copyWith(
    1547           0 :         extentOffset: selectedLine.baseOffset,
    1548             :       );
    1549             :     }
    1550             : 
    1551           0 :     _setSelection(nextSelection, cause);
    1552             :   }
    1553             : 
    1554             :   /// Keeping [selection]'s [TextSelection.baseOffset] fixed, move the
    1555             :   /// [TextSelection.extentOffset] down.
    1556             :   ///
    1557             :   /// If [selectionEnabled] is false, keeps the selection collapsed and moves it
    1558             :   /// down.
    1559             :   ///
    1560             :   /// See also:
    1561             :   ///
    1562             :   ///   * [extendSelectionUp], which is same but in the opposite direction.
    1563           1 :   void extendSelectionDown(SelectionChangedCause cause) {
    1564           1 :     assert(selection != null);
    1565             : 
    1566           1 :     if (!selectionEnabled) {
    1567           0 :       return moveSelectionDown(cause);
    1568             :     }
    1569             : 
    1570           1 :     final nextSelection = _extendGivenSelectionDown(
    1571           1 :       selection!,
    1572           1 :       _plainText,
    1573             :     );
    1574           2 :     if (nextSelection == selection) {
    1575             :       return;
    1576             :     }
    1577           4 :     final distance = nextSelection.extentOffset - selection!.extentOffset;
    1578           2 :     _cursorResetLocation += distance;
    1579           1 :     _setSelection(nextSelection, cause);
    1580             :   }
    1581             : 
    1582             :   /// Extend the current [selection] to the end of [TextSelection.extentOffset]'s
    1583             :   /// line.
    1584             :   ///
    1585             :   /// Uses [TextSelection.baseOffset] as a pivot point and doesn't change it. If
    1586             :   /// [TextSelection.extentOffset] is above [TextSelection.baseOffset], then
    1587             :   /// collapses the selection.
    1588             :   ///
    1589             :   /// If [selectionEnabled] is false, keeps the selection collapsed and moves it
    1590             :   /// down by line.
    1591             :   ///
    1592             :   /// See also:
    1593             :   ///
    1594             :   ///   * [extendSelectionUpByLine], which is same but in the opposite
    1595             :   ///     direction.
    1596             :   ///   * [expandSelectionDownByLine], which strictly grows the selection
    1597             :   ///     regardless of the order.
    1598           0 :   void extendSelectionDownByLine(SelectionChangedCause cause) {
    1599           0 :     assert(selection != null);
    1600             : 
    1601           0 :     if (!selectionEnabled) {
    1602           0 :       return moveSelectionDownByLine(cause);
    1603             :     }
    1604             : 
    1605             :     final startPoint =
    1606           0 :         nextCharacter(selection!.extentOffset, _plainText, false);
    1607           0 :     final selectedLine = _getLineAtOffset(TextPosition(offset: startPoint));
    1608             : 
    1609             :     late final TextSelection nextSelection;
    1610           0 :     if (selection!.extentOffset < selection!.baseOffset) {
    1611           0 :       nextSelection = selection!.copyWith(
    1612           0 :         extentOffset: selection!.baseOffset,
    1613             :       );
    1614             :     } else {
    1615           0 :       nextSelection = selection!.copyWith(
    1616           0 :         extentOffset: selectedLine.extentOffset,
    1617             :       );
    1618             :     }
    1619             : 
    1620           0 :     _setSelection(nextSelection, cause);
    1621             :   }
    1622             : 
    1623             :   /// Keeping [selection]'s [TextSelection.baseOffset] fixed, move the
    1624             :   /// [TextSelection.extentOffset] left by one
    1625             :   /// line.
    1626             :   ///
    1627             :   /// If [selectionEnabled] is false, keeps the selection collapsed and moves it
    1628             :   /// left.
    1629             :   ///
    1630             :   /// See also:
    1631             :   ///
    1632             :   ///   * [extendSelectionLeft], which is the same but in the opposite
    1633             :   ///     direction.
    1634           0 :   void extendSelectionLeft(SelectionChangedCause cause) {
    1635           0 :     assert(selection != null);
    1636             : 
    1637             :     // If the selection is collapsed at the beginning of the field already, then
    1638             :     // nothing happens.
    1639           0 :     if (selection!.isCollapsed && selection!.extentOffset <= 0.0) {
    1640             :       return;
    1641             :     }
    1642           0 :     if (!selectionEnabled) {
    1643           0 :       return moveSelectionLeft(cause);
    1644             :     }
    1645             : 
    1646           0 :     final positionLeft = _getTextPositionLeft(selection!.extentOffset);
    1647             :     late final TextSelection nextSelection;
    1648           0 :     if (positionLeft.offset == selection!.extentOffset) {
    1649           0 :       nextSelection = selection!.copyWith(
    1650             :         extentOffset: 0,
    1651             :       );
    1652           0 :       _wasSelectingHorizontallyWithKeyboard = true;
    1653           0 :     } else if (_wasSelectingHorizontallyWithKeyboard) {
    1654           0 :       nextSelection = selection!.copyWith(
    1655           0 :         baseOffset: selection!.baseOffset,
    1656           0 :         extentOffset: _cursorResetLocation,
    1657             :       );
    1658           0 :       _wasSelectingHorizontallyWithKeyboard = false;
    1659             :     } else {
    1660           0 :       nextSelection = selection!.copyWith(
    1661           0 :         baseOffset: selection!.baseOffset,
    1662           0 :         extentOffset: positionLeft.offset,
    1663             :       );
    1664           0 :       _cursorResetLocation = nextSelection.extentOffset;
    1665             :     }
    1666             : 
    1667           0 :     _setSelection(nextSelection, cause);
    1668             :   }
    1669             : 
    1670             :   /// Expand the current [selection] to the start of the field.
    1671             :   ///
    1672             :   /// The selection will never shrink. The [TextSelection.extentOffset] will
    1673             :   /// always be at the start of the field, regardless of the original order of
    1674             :   /// [TextSelection.baseOffset] and [TextSelection.extentOffset].
    1675             :   ///
    1676             :   /// If [selectionEnabled] is false, keeps the selection collapsed and moves it
    1677             :   /// to the start.
    1678             :   ///
    1679             :   /// See also:
    1680             :   ///
    1681             :   ///   * [expandSelectionToEnd], which is the same but in the opposite
    1682             :   ///     direction.
    1683           0 :   void expandSelectionToStart(SelectionChangedCause cause) {
    1684           0 :     assert(selection != null);
    1685             : 
    1686           0 :     if (selection!.extentOffset == 0) {
    1687             :       return;
    1688             :     }
    1689           0 :     if (!selectionEnabled) {
    1690           0 :       return moveSelectionToStart(cause);
    1691             :     }
    1692             : 
    1693           0 :     final lastOffset = math.max(
    1694             :         0,
    1695           0 :         math.max(
    1696           0 :           selection!.baseOffset,
    1697           0 :           selection!.extentOffset,
    1698             :         ));
    1699           0 :     final nextSelection = TextSelection(
    1700             :       baseOffset: lastOffset,
    1701             :       extentOffset: 0,
    1702             :     );
    1703           0 :     _setSelection(nextSelection, cause);
    1704             :   }
    1705             : 
    1706             :   /// Expand the current [selection] to the start of the line.
    1707             :   ///
    1708             :   /// The selection will never shrink. The upper offset will be expanded to the
    1709             :   /// beginning of its line, and the original order of baseOffset and
    1710             :   /// [TextSelection.extentOffset] will be preserved.
    1711             :   ///
    1712             :   /// If [selectionEnabled] is false, keeps the selection collapsed and moves it
    1713             :   /// left by line.
    1714             :   ///
    1715             :   /// See also:
    1716             :   ///
    1717             :   ///   * [expandSelectionDownByLine], which is the same but in the opposite
    1718             :   ///     direction.
    1719           0 :   void expandSelectionUpByLine(SelectionChangedCause cause) {
    1720           0 :     assert(selection != null);
    1721             : 
    1722           0 :     if (!selectionEnabled) {
    1723           0 :       return moveSelectionUpByLine(cause);
    1724             :     }
    1725             : 
    1726             :     final firstOffset =
    1727           0 :         math.min(selection!.baseOffset, selection!.extentOffset);
    1728           0 :     final startPoint = previousCharacter(firstOffset, _plainText, false);
    1729           0 :     final selectedLine = _getLineAtOffset(TextPosition(offset: startPoint));
    1730             : 
    1731             :     late final TextSelection nextSelection;
    1732           0 :     if (selection!.extentOffset <= selection!.baseOffset) {
    1733           0 :       nextSelection = selection!.copyWith(
    1734           0 :         extentOffset: selectedLine.baseOffset,
    1735             :       );
    1736             :     } else {
    1737           0 :       nextSelection = selection!.copyWith(
    1738           0 :         baseOffset: selectedLine.baseOffset,
    1739             :       );
    1740             :     }
    1741             : 
    1742           0 :     _setSelection(nextSelection, cause);
    1743             :   }
    1744             : 
    1745             :   /// Extend the current [selection] to the previous start of a word.
    1746             :   ///
    1747             :   /// By default, `includeWhitespace` is set to true, meaning that whitespace
    1748             :   /// can be considered a word in itself.  If set to false, the selection will
    1749             :   /// be extended past any whitespace and the first word following the
    1750             :   /// whitespace.
    1751             :   ///
    1752             :   /// The `stopAtReversal` parameter is false by default, meaning that it's
    1753             :   /// ok for the base and extent to flip their order here. If set to true, then
    1754             :   /// the selection will collapse when it would otherwise reverse its order. A
    1755             :   /// selection that is already collapsed is not affected by this parameter.
    1756             :   ///
    1757             :   /// See also:
    1758             :   ///
    1759             :   ///   * [extendSelectionDownByWord], which is the same but in the opposite
    1760             :   ///     direction.
    1761           0 :   void extendSelectionUpByWord(
    1762             :     SelectionChangedCause cause, [
    1763             :     bool includeWhitespace = true,
    1764             :     bool stopAtReversal = false,
    1765             :   ]) {
    1766           0 :     assert(selection != null);
    1767             : 
    1768             :     // When the text is obscured, the whole thing is treated as one big word.
    1769           0 :     if (obscureText) {
    1770           0 :       return _extendSelectionToStart(cause);
    1771             :     }
    1772             : 
    1773             :     assert(
    1774           0 :       _textLayoutLastMaxHeight == constraints.maxHeight &&
    1775           0 :           _textLayoutLastMinHeight == constraints.minHeight,
    1776           0 :       'Last height ($_textLayoutLastMinHeight, $_textLayoutLastMaxHeight) not the same as max height constraint (${constraints.minHeight}, ${constraints.maxHeight}).',
    1777             :     );
    1778           0 :     final nextSelection = _extendGivenSelectionUpByWord(
    1779           0 :       _textPainter,
    1780           0 :       selection!,
    1781             :       includeWhitespace,
    1782             :       stopAtReversal,
    1783             :     );
    1784           0 :     if (nextSelection == selection) {
    1785             :       return;
    1786             :     }
    1787           0 :     _setSelection(nextSelection, cause);
    1788             :   }
    1789             : 
    1790             :   /// Extend the current [selection] to the next end of a word.
    1791             :   ///
    1792             :   /// By default, `includeWhitespace` is set to true, meaning that whitespace
    1793             :   /// can be considered a word in itself.  If set to false, the selection will
    1794             :   /// be extended past any whitespace and the first word following the
    1795             :   /// whitespace.
    1796             :   ///
    1797             :   /// See also:
    1798             :   ///
    1799             :   ///   * [extendSelectionUpByWord], which is the same but in the opposite
    1800             :   ///     direction.
    1801           0 :   void extendSelectionDownByWord(
    1802             :     SelectionChangedCause cause, [
    1803             :     bool includeWhitespace = true,
    1804             :     bool stopAtReversal = false,
    1805             :   ]) {
    1806           0 :     assert(selection != null);
    1807             : 
    1808             :     // When the text is obscured, the whole thing is treated as one big word.
    1809           0 :     if (obscureText) {
    1810           0 :       return _extendSelectionToEnd(cause);
    1811             :     }
    1812             : 
    1813             :     assert(
    1814           0 :       _textLayoutLastMaxHeight == constraints.maxHeight &&
    1815           0 :           _textLayoutLastMinHeight == constraints.minHeight,
    1816           0 :       'Last height ($_textLayoutLastMinHeight, $_textLayoutLastMaxHeight) not the same as max height constraint (${constraints.minHeight}, ${constraints.maxHeight}).',
    1817             :     );
    1818           0 :     final nextSelection = _extendGivenSelectionDownByWord(
    1819           0 :       _textPainter,
    1820           0 :       selection!,
    1821             :       includeWhitespace,
    1822             :       stopAtReversal,
    1823             :     );
    1824           0 :     if (nextSelection == selection) {
    1825             :       return;
    1826             :     }
    1827           0 :     _setSelection(nextSelection, cause);
    1828             :   }
    1829             : 
    1830             :   /// Expand the current [selection] to the end of the line.
    1831             :   ///
    1832             :   /// The selection will never shrink. The lower offset will be expanded to the
    1833             :   /// end of its line and the original order of [TextSelection.baseOffset] and
    1834             :   /// [TextSelection.extentOffset] will be preserved.
    1835             :   ///
    1836             :   /// If [selectionEnabled] is false, keeps the selection collapsed and moves it
    1837             :   /// down by line.
    1838             :   ///
    1839             :   /// See also:
    1840             :   ///
    1841             :   ///   * [expandSelectionUpByLine], which is the same but in the opposite
    1842             :   ///     direction.
    1843           0 :   void expandSelectionDownByLine(SelectionChangedCause cause) {
    1844           0 :     assert(selection != null);
    1845             : 
    1846           0 :     if (!selectionEnabled) {
    1847           0 :       return moveSelectionDownByLine(cause);
    1848             :     }
    1849             : 
    1850           0 :     final lastOffset = math.max(selection!.baseOffset, selection!.extentOffset);
    1851           0 :     final startPoint = nextCharacter(lastOffset, _plainText, false);
    1852           0 :     final selectedLine = _getLineAtOffset(TextPosition(offset: startPoint));
    1853             : 
    1854             :     late final TextSelection nextSelection;
    1855           0 :     if (selection!.extentOffset >= selection!.baseOffset) {
    1856           0 :       nextSelection = selection!.copyWith(
    1857           0 :         extentOffset: selectedLine.extentOffset,
    1858             :       );
    1859             :     } else {
    1860           0 :       nextSelection = selection!.copyWith(
    1861           0 :         baseOffset: selectedLine.extentOffset,
    1862             :       );
    1863             :     }
    1864             : 
    1865           0 :     _setSelection(nextSelection, cause);
    1866             :   }
    1867             : 
    1868             :   /// Move the current [selection] to the next line.
    1869             :   ///
    1870             :   /// See also:
    1871             :   ///
    1872             :   ///   * [moveSelectionLeft], which is the same but in the opposite direction.
    1873           0 :   void moveSelectionRight(SelectionChangedCause cause) {
    1874           0 :     assert(selection != null);
    1875             : 
    1876             :     // If the selection is collapsed at the end of the field already, then
    1877             :     // nothing happens.
    1878           0 :     if (selection!.isCollapsed &&
    1879           0 :         selection!.extentOffset >= _plainText.length) {
    1880             :       return;
    1881             :     }
    1882             : 
    1883           0 :     final positionRight = _getTextPositionRight(selection!.extentOffset);
    1884             : 
    1885             :     late final TextSelection nextSelection;
    1886           0 :     if (positionRight.offset == selection!.extentOffset) {
    1887           0 :       nextSelection = selection!.copyWith(
    1888           0 :         baseOffset: _plainText.length,
    1889           0 :         extentOffset: _plainText.length,
    1890             :       );
    1891           0 :       _wasSelectingHorizontallyWithKeyboard = false;
    1892             :     } else {
    1893           0 :       nextSelection = TextSelection.fromPosition(positionRight);
    1894           0 :       _cursorResetLocation = nextSelection.extentOffset;
    1895             :     }
    1896             : 
    1897           0 :     _setSelection(nextSelection, cause);
    1898             :   }
    1899             : 
    1900             :   /// Move the current [selection] up by one character.
    1901             :   ///
    1902             :   /// See also:
    1903             :   ///
    1904             :   ///   * [moveSelectionDown], which is the same but in the opposite direction.
    1905           1 :   void moveSelectionUp(SelectionChangedCause cause) {
    1906           1 :     assert(selection != null);
    1907             : 
    1908           1 :     final nextSelection = _moveGivenSelectionUp(
    1909           1 :       selection!,
    1910           1 :       _plainText,
    1911             :     );
    1912           2 :     if (nextSelection == selection) {
    1913             :       return;
    1914             :     }
    1915           2 :     _cursorResetLocation -=
    1916           4 :         selection!.extentOffset - nextSelection.extentOffset;
    1917           1 :     _setSelection(nextSelection, cause);
    1918             :   }
    1919             : 
    1920             :   /// Move the current [selection] to the top of the current line.
    1921             :   ///
    1922             :   /// See also:
    1923             :   ///
    1924             :   ///   * [moveSelectionDownByLine], which is the same but in the opposite
    1925             :   ///     direction.
    1926           1 :   void moveSelectionUpByLine(SelectionChangedCause cause) {
    1927           1 :     assert(selection != null);
    1928             : 
    1929             :     // If the previous character is the edge of a line, don't do anything.
    1930             :     final previousPoint =
    1931           4 :         previousCharacter(selection!.extentOffset, _plainText, true);
    1932           2 :     final line = _getLineAtOffset(TextPosition(offset: previousPoint));
    1933           2 :     if (line.extentOffset == previousPoint) {
    1934             :       return;
    1935             :     }
    1936             : 
    1937             :     // When going up, we want to skip over any whitespace before the line,
    1938             :     // so we go back to the first non-whitespace before asking for the line
    1939             :     // bounds, since _getLineAtOffset finds the line boundaries without
    1940             :     // including whitespace (like the newline).
    1941             :     final startPoint =
    1942           4 :         previousCharacter(selection!.extentOffset, _plainText, false);
    1943           2 :     final selectedLine = _getLineAtOffset(TextPosition(offset: startPoint));
    1944           1 :     final nextSelection = TextSelection.collapsed(
    1945           1 :       offset: selectedLine.baseOffset,
    1946             :     );
    1947             : 
    1948           1 :     _setSelection(nextSelection, cause);
    1949             :   }
    1950             : 
    1951             :   /// Move the current [selection] to the previous start of a word.
    1952             :   ///
    1953             :   /// By default, includeWhitespace is set to true, meaning that whitespace can
    1954             :   /// be considered a word in itself.  If set to false, the selection will be
    1955             :   /// moved past any whitespace and the first word following the whitespace.
    1956             :   ///
    1957             :   /// See also:
    1958             :   ///
    1959             :   ///   * [moveSelectionDownByWord], which is the same but in the opposite
    1960             :   ///     direction.
    1961           1 :   void moveSelectionUpByWord(SelectionChangedCause cause,
    1962             :       [bool includeWhitespace = true]) {
    1963           1 :     assert(selection != null);
    1964             : 
    1965             :     // When the text is obscured, the whole thing is treated as one big word.
    1966           1 :     if (obscureText) {
    1967           0 :       return moveSelectionToStart(cause);
    1968             :     }
    1969             : 
    1970             :     assert(
    1971           4 :       _textLayoutLastMaxHeight == constraints.maxHeight &&
    1972           4 :           _textLayoutLastMinHeight == constraints.minHeight,
    1973           0 :       'Last height ($_textLayoutLastMinHeight, $_textLayoutLastMaxHeight) not the same as max height constraint (${constraints.minHeight}, ${constraints.maxHeight}).',
    1974             :     );
    1975           1 :     final nextSelection = _moveGivenSelectionUpByWord(
    1976           1 :       _textPainter,
    1977           1 :       selection!,
    1978             :       includeWhitespace,
    1979             :     );
    1980           2 :     if (nextSelection == selection) {
    1981             :       return;
    1982             :     }
    1983           1 :     _setSelection(nextSelection, cause);
    1984             :   }
    1985             : 
    1986             :   /// Move the current [selection] down by one character.
    1987             :   ///
    1988             :   /// See also:
    1989             :   ///
    1990             :   ///   * [moveSelectionUp], which is the same but in the opposite direction.
    1991           1 :   void moveSelectionDown(SelectionChangedCause cause) {
    1992           1 :     assert(selection != null);
    1993             : 
    1994           1 :     final nextSelection = _moveGivenSelectionDown(
    1995           1 :       selection!,
    1996           1 :       _plainText,
    1997             :     );
    1998           2 :     if (nextSelection == selection) {
    1999             :       return;
    2000             :     }
    2001           1 :     _setSelection(nextSelection, cause);
    2002             :   }
    2003             : 
    2004             :   /// Move the current [selection] to the bottommost point of the current line.
    2005             :   ///
    2006             :   /// See also:
    2007             :   ///
    2008             :   ///   * [moveSelectionUpByLine], which is the same but in the opposite
    2009             :   ///     direction.
    2010           1 :   void moveSelectionDownByLine(SelectionChangedCause cause) {
    2011           1 :     assert(selection != null);
    2012             : 
    2013             :     // If already at the bottom edge of the line, do nothing.
    2014           2 :     final currentLine = _getLineAtOffset(TextPosition(
    2015           2 :       offset: selection!.extentOffset,
    2016             :     ));
    2017           4 :     if (currentLine.extentOffset == selection!.extentOffset) {
    2018             :       return;
    2019             :     }
    2020             : 
    2021             :     // When going down, we want to skip over any whitespace after the line,
    2022             :     // so we go forward to the first non-whitespace character before asking
    2023             :     // for the line bounds, since _getLineAtOffset finds the line
    2024             :     // boundaries without including whitespace (like the newline).
    2025             :     final startPoint =
    2026           4 :         nextCharacter(selection!.extentOffset, _plainText, false);
    2027           2 :     final selectedLine = _getLineAtOffset(TextPosition(offset: startPoint));
    2028           1 :     final nextSelection = TextSelection.collapsed(
    2029           1 :       offset: selectedLine.extentOffset,
    2030             :     );
    2031             : 
    2032           1 :     _setSelection(nextSelection, cause);
    2033             :   }
    2034             : 
    2035             :   /// Move the current [selection] to the next end of a word.
    2036             :   ///
    2037             :   /// By default, includeWhitespace is set to true, meaning that whitespace can
    2038             :   /// be considered a word in itself.  If set to false, the selection will be
    2039             :   /// moved past any whitespace and the first word following the whitespace.
    2040             :   ///
    2041             :   /// See also:
    2042             :   ///
    2043             :   ///   * [moveSelectionUpByWord], which is the same but in the opposite
    2044             :   ///     direction.
    2045           1 :   void moveSelectionDownByWord(
    2046             :     SelectionChangedCause cause, [
    2047             :     bool includeWhitespace = true,
    2048             :   ]) {
    2049           1 :     assert(selection != null);
    2050             : 
    2051             :     // When the text is obscured, the whole thing is treated as one big word.
    2052           1 :     if (obscureText) {
    2053           0 :       return moveSelectionToEnd(cause);
    2054             :     }
    2055             : 
    2056             :     assert(
    2057           4 :       _textLayoutLastMaxHeight == constraints.maxHeight &&
    2058           4 :           _textLayoutLastMinHeight == constraints.minHeight,
    2059           0 :       'Last height ($_textLayoutLastMinHeight, $_textLayoutLastMaxHeight) not the same as max height constraint (${constraints.minHeight}, ${constraints.maxHeight}).',
    2060             :     );
    2061           1 :     final nextSelection = _moveGivenSelectionDownByWord(
    2062           1 :       _textPainter,
    2063           1 :       selection!,
    2064             :       includeWhitespace,
    2065             :     );
    2066           2 :     if (nextSelection == selection) {
    2067             :       return;
    2068             :     }
    2069           1 :     _setSelection(nextSelection, cause);
    2070             :   }
    2071             : 
    2072             :   /// Move the current [selection] to the end of the field.
    2073             :   ///
    2074             :   /// See also:
    2075             :   ///
    2076             :   ///   * [moveSelectionToStart], which is the same but in the opposite
    2077             :   ///     direction.
    2078           0 :   void moveSelectionToEnd(SelectionChangedCause cause) {
    2079           0 :     assert(selection != null);
    2080             : 
    2081           0 :     if (selection!.isCollapsed &&
    2082           0 :         selection!.extentOffset == _plainText.length) {
    2083             :       return;
    2084             :     }
    2085           0 :     final nextSelection = TextSelection.collapsed(
    2086           0 :       offset: _plainText.length,
    2087             :     );
    2088           0 :     _setSelection(nextSelection, cause);
    2089             :   }
    2090             : 
    2091             :   /// Move the current [selection] to the start of the field.
    2092             :   ///
    2093             :   /// See also:
    2094             :   ///
    2095             :   ///   * [moveSelectionToEnd], which is the same but in the opposite direction.
    2096           0 :   void moveSelectionToStart(SelectionChangedCause cause) {
    2097           0 :     assert(selection != null);
    2098             : 
    2099           0 :     if (selection!.isCollapsed && selection!.extentOffset == 0) {
    2100             :       return;
    2101             :     }
    2102             :     const nextSelection = TextSelection.collapsed(offset: 0);
    2103           0 :     _setSelection(nextSelection, cause);
    2104             :   }
    2105             : 
    2106             :   /// Move the current [selection] left by one line.
    2107             :   ///
    2108             :   /// See also:
    2109             :   ///
    2110             :   ///   * [moveSelectionRight], which is the same but in the opposite direction.
    2111           0 :   void moveSelectionLeft(SelectionChangedCause cause) {
    2112           0 :     assert(selection != null);
    2113             : 
    2114             :     // If the selection is collapsed at the beginning of the field already, then
    2115             :     // nothing happens.
    2116           0 :     if (selection!.isCollapsed && selection!.extentOffset <= 0.0) {
    2117             :       return;
    2118             :     }
    2119             : 
    2120           0 :     final positionLeft = _getTextPositionLeft(selection!.extentOffset);
    2121             :     late final TextSelection nextSelection;
    2122           0 :     if (positionLeft.offset == selection!.extentOffset) {
    2123           0 :       nextSelection = selection!.copyWith(baseOffset: 0, extentOffset: 0);
    2124           0 :       _wasSelectingHorizontallyWithKeyboard = false;
    2125             :     } else {
    2126           0 :       nextSelection = selection!.copyWith(
    2127           0 :         baseOffset: positionLeft.offset,
    2128           0 :         extentOffset: positionLeft.offset,
    2129             :       );
    2130           0 :       _cursorResetLocation = nextSelection.extentOffset;
    2131             :     }
    2132             : 
    2133           0 :     _setSelection(nextSelection, cause);
    2134             :   }
    2135             : 
    2136             :   // Handles shortcut functionality including cut, copy, paste and select all
    2137             :   // using control/command + (X, C, V, A).
    2138           2 :   Future<void> _handleShortcuts(LogicalKeyboardKey key) async {
    2139           6 :     final selection = textSelectionDelegate.textEditingValue.selection;
    2140           6 :     final text = textSelectionDelegate.textEditingValue.text;
    2141           4 :     assert(_shortcutKeys.contains(key), 'shortcut key $key not recognized.');
    2142           2 :     if (key == LogicalKeyboardKey.keyC) {
    2143           1 :       if (!selection.isCollapsed) {
    2144             :         // ignore: unawaited_futures
    2145           3 :         Clipboard.setData(ClipboardData(text: selection.textInside(text)));
    2146             :       }
    2147             :       return;
    2148             :     }
    2149             :     TextEditingValue? value;
    2150           3 :     if (key == LogicalKeyboardKey.keyX && !_readOnly) {
    2151           0 :       if (!selection.isCollapsed) {
    2152             :         // ignore: unawaited_futures
    2153           0 :         Clipboard.setData(ClipboardData(text: selection.textInside(text)));
    2154           0 :         value = TextEditingValue(
    2155           0 :           text: selection.textBefore(text) + selection.textAfter(text),
    2156           0 :           selection: TextSelection.collapsed(
    2157           0 :               offset: math.min(selection.start, selection.end)),
    2158             :         );
    2159             :       }
    2160           3 :     } else if (key == LogicalKeyboardKey.keyV && !_readOnly) {
    2161             :       // Snapshot the input before using `await`.
    2162             :       // See https://github.com/flutter/flutter/issues/11427
    2163           0 :       final data = await Clipboard.getData(Clipboard.kTextPlain);
    2164             :       if (data != null) {
    2165           0 :         value = TextEditingValue(
    2166           0 :           text: selection.textBefore(text) +
    2167           0 :               data.text! +
    2168           0 :               selection.textAfter(text),
    2169           0 :           selection: TextSelection.collapsed(
    2170             :             offset:
    2171           0 :                 math.min(selection.start, selection.end) + data.text!.length,
    2172             :           ),
    2173             :         );
    2174             :       }
    2175           2 :     } else if (key == LogicalKeyboardKey.keyA) {
    2176           2 :       value = TextEditingValue(
    2177             :         text: text,
    2178           2 :         selection: selection.copyWith(
    2179             :           baseOffset: 0,
    2180           8 :           extentOffset: textSelectionDelegate.textEditingValue.text.length,
    2181             :         ),
    2182             :       );
    2183             :     }
    2184             :     if (value != null) {
    2185           2 :       _setTextEditingValue(
    2186             :         value,
    2187             :         SelectionChangedCause.keyboard,
    2188             :       );
    2189             :     }
    2190             :   }
    2191             : 
    2192           2 :   void _handleDelete({required bool forward}) {
    2193           6 :     final selection = textSelectionDelegate.textEditingValue.selection;
    2194           6 :     final text = textSelectionDelegate.textEditingValue.text;
    2195           2 :     assert(_selection != null);
    2196           4 :     if (_readOnly || !selection.isValid) {
    2197             :       return;
    2198             :     }
    2199           1 :     var textBefore = selection.textBefore(text);
    2200           1 :     var textAfter = selection.textAfter(text);
    2201           3 :     var cursorPosition = math.min(selection.start, selection.end);
    2202             :     // If not deleting a selection, delete the next/previous character.
    2203           1 :     if (selection.isCollapsed) {
    2204           0 :       if (!forward && textBefore.isNotEmpty) {
    2205             :         final characterBoundary =
    2206           0 :             previousCharacter(textBefore.length, textBefore);
    2207           0 :         textBefore = textBefore.substring(0, characterBoundary);
    2208             :         cursorPosition = characterBoundary;
    2209             :       }
    2210           1 :       if (forward && textAfter.isNotEmpty) {
    2211           1 :         final deleteCount = nextCharacter(0, textAfter);
    2212           1 :         textAfter = textAfter.substring(deleteCount);
    2213             :       }
    2214             :     }
    2215           1 :     final newSelection = TextSelection.collapsed(offset: cursorPosition);
    2216           1 :     _setTextEditingValue(
    2217           1 :       TextEditingValue(
    2218           1 :         text: textBefore + textAfter,
    2219             :         selection: newSelection,
    2220             :       ),
    2221             :       SelectionChangedCause.keyboard,
    2222             :     );
    2223             :   }
    2224             : 
    2225           3 :   @override
    2226             :   void markNeedsPaint() {
    2227           3 :     super.markNeedsPaint();
    2228             :     // Tell the painers to repaint since text layout may have changed.
    2229           6 :     _foregroundRenderObject?.markNeedsPaint();
    2230           6 :     _backgroundRenderObject?.markNeedsPaint();
    2231             :   }
    2232             : 
    2233             :   /// Marks the render object as needing to be laid out again and have its text
    2234             :   /// metrics recomputed.
    2235             :   ///
    2236             :   /// Implies [markNeedsLayout].
    2237           3 :   @protected
    2238             :   void markNeedsTextLayout() {
    2239           3 :     _textLayoutLastMaxHeight = null;
    2240           3 :     _textLayoutLastMinHeight = null;
    2241           3 :     markNeedsLayout();
    2242             :   }
    2243             : 
    2244           0 :   @override
    2245             :   void systemFontsDidChange() {
    2246           0 :     super.systemFontsDidChange();
    2247           0 :     _textPainter.markNeedsLayout();
    2248           0 :     _textLayoutLastMaxHeight = null;
    2249           0 :     _textLayoutLastMinHeight = null;
    2250             :   }
    2251             : 
    2252             :   String? _cachedPlainText;
    2253             :   // Returns a plain text version of the text in the painter.
    2254             :   //
    2255             :   // Returns the obscured text when [obscureText] is true. See
    2256             :   // [obscureText] and [obscuringCharacter].
    2257           3 :   String get _plainText {
    2258           3 :     _cachedPlainText ??=
    2259           9 :         _textPainter.text!.toPlainText(includeSemanticsLabels: false);
    2260           3 :     return _cachedPlainText!;
    2261             :   }
    2262             : 
    2263             :   /// The text to display.
    2264           9 :   TextSpan? get text => _textPainter.text;
    2265             :   final MongolTextPainter _textPainter;
    2266           3 :   set text(TextSpan? value) {
    2267           9 :     if (_textPainter.text == value) return;
    2268           6 :     _textPainter.text = value;
    2269           3 :     _cachedPlainText = null;
    2270           3 :     markNeedsTextLayout();
    2271           3 :     markNeedsSemanticsUpdate();
    2272             :   }
    2273             : 
    2274             :   /// How the text should be aligned vertically.
    2275             :   ///
    2276             :   /// This must not be null.
    2277           3 :   MongolTextAlign get textAlign => _textPainter.textAlign;
    2278           3 :   set textAlign(MongolTextAlign value) {
    2279           9 :     if (_textPainter.textAlign == value) return;
    2280           4 :     _textPainter.textAlign = value;
    2281           2 :     markNeedsTextLayout();
    2282             :   }
    2283             : 
    2284             :   /// The color to use when painting the cursor.
    2285           6 :   Color? get cursorColor => _caretPainter.caretColor;
    2286           3 :   set cursorColor(Color? value) {
    2287           6 :     _caretPainter.caretColor = value;
    2288             :   }
    2289             : 
    2290             :   /// Whether to paint the cursor.
    2291           6 :   ValueNotifier<bool> get showCursor => _showCursor;
    2292             :   ValueNotifier<bool> _showCursor;
    2293           3 :   set showCursor(ValueNotifier<bool> value) {
    2294           6 :     if (_showCursor == value) return;
    2295           4 :     if (attached) _showCursor.removeListener(_showHideCursor);
    2296           1 :     _showCursor = value;
    2297           1 :     if (attached) {
    2298           1 :       _showHideCursor();
    2299           3 :       _showCursor.addListener(_showHideCursor);
    2300             :     }
    2301             :   }
    2302             : 
    2303           3 :   void _showHideCursor() {
    2304          12 :     _caretPainter.shouldPaint = showCursor.value;
    2305             :   }
    2306             : 
    2307             :   /// Whether the editable is currently focused.
    2308           6 :   bool get hasFocus => _hasFocus;
    2309             :   bool _hasFocus = false;
    2310             :   bool _listenerAttached = false;
    2311           3 :   set hasFocus(bool value) {
    2312           6 :     if (_hasFocus == value) return;
    2313           3 :     _hasFocus = value;
    2314           3 :     markNeedsSemanticsUpdate();
    2315             : 
    2316           3 :     if (!attached) {
    2317           1 :       assert(!_listenerAttached);
    2318             :       return;
    2319             :     }
    2320             : 
    2321           3 :     if (_hasFocus) {
    2322           3 :       assert(!_listenerAttached);
    2323             :       // TODO(justinmc): This listener should be ported to Actions and removed.
    2324             :       // https://github.com/flutter/flutter/issues/75004
    2325           9 :       RawKeyboard.instance.addListener(_handleKeyEvent);
    2326           3 :       _listenerAttached = true;
    2327             :     } else {
    2328           2 :       assert(_listenerAttached);
    2329             :       // TODO(justinmc): This listener should be ported to Actions and removed.
    2330             :       // https://github.com/flutter/flutter/issues/75004
    2331           6 :       RawKeyboard.instance.removeListener(_handleKeyEvent);
    2332           2 :       _listenerAttached = false;
    2333             :     }
    2334             :   }
    2335             : 
    2336             :   /// Whether this rendering object will take a full line regardless the
    2337             :   /// text height.
    2338           6 :   bool get forceLine => _forceLine;
    2339             :   bool _forceLine = false;
    2340           2 :   set forceLine(bool value) {
    2341           4 :     if (_forceLine == value) return;
    2342           0 :     _forceLine = value;
    2343           0 :     markNeedsLayout();
    2344             :   }
    2345             : 
    2346             :   /// Whether this rendering object is read only.
    2347           6 :   bool get readOnly => _readOnly;
    2348             :   bool _readOnly = false;
    2349           2 :   set readOnly(bool value) {
    2350           4 :     if (_readOnly == value) return;
    2351           2 :     _readOnly = value;
    2352           2 :     markNeedsSemanticsUpdate();
    2353             :   }
    2354             : 
    2355             :   /// The maximum number of lines for the text to span, wrapping if necessary.
    2356             :   ///
    2357             :   /// If this is 1 (the default), the text will not wrap, but will extend
    2358             :   /// indefinitely instead.
    2359             :   ///
    2360             :   /// If this is null, there is no limit to the number of lines.
    2361             :   ///
    2362             :   /// When this is not null, the intrinsic width of the render object is the
    2363             :   /// width of one line of text multiplied by this value. In other words, this
    2364             :   /// also controls the width of the actual editing widget.
    2365           6 :   int? get maxLines => _maxLines;
    2366             :   int? _maxLines;
    2367             : 
    2368             :   /// The value may be null. If it is not null, then it must be greater than zero.
    2369           2 :   set maxLines(int? value) {
    2370           2 :     assert(value == null || value > 0);
    2371           4 :     if (maxLines == value) return;
    2372           1 :     _maxLines = value;
    2373           1 :     markNeedsTextLayout();
    2374             :   }
    2375             : 
    2376             :   /// The minimum number of lines to occupy when the content spans fewer lines.
    2377             :   ///
    2378             :   /// If this is null (default), text container starts with enough horizontal space
    2379             :   /// for one line and grows to accommodate additional lines as they are entered.
    2380             :   ///
    2381             :   /// This can be used in combination with [maxLines] for a varying set of behaviors.
    2382             :   ///
    2383             :   /// If the value is set, it must be greater than zero. If the value is greater
    2384             :   /// than 1, [maxLines] should also be set to either null or greater than
    2385             :   /// this value.
    2386             :   ///
    2387             :   /// When [maxLines] is set as well, the width will grow between the indicated
    2388             :   /// range of lines. When [maxLines] is null, it will grow as wide as needed,
    2389             :   /// starting from [minLines].
    2390             :   ///
    2391             :   /// A few examples of behaviors possible with [minLines] and [maxLines] are as follows.
    2392             :   /// These apply equally to `MongolTextField`, `MongolTextFormField`,
    2393             :   /// and `MongolEditableText`.
    2394             :   ///
    2395             :   /// Input that always occupies at least 2 lines and has an infinite max.
    2396             :   /// Expands horizontally as needed.
    2397             :   /// ```dart
    2398             :   /// MongolTextField(minLines: 2)
    2399             :   /// ```
    2400             :   ///
    2401             :   /// Input whose width starts from 2 lines and grows up to 4 lines at which
    2402             :   /// point the width limit is reached. If additional lines are entered it will
    2403             :   /// scroll horizontally.
    2404             :   /// ```dart
    2405             :   /// TextField(minLines:2, maxLines: 4)
    2406             :   /// ```
    2407             :   ///
    2408             :   /// See the examples in [maxLines] for the complete picture of how [maxLines]
    2409             :   /// and [minLines] interact to produce various behaviors.
    2410             :   ///
    2411             :   /// Defaults to null.
    2412           6 :   int? get minLines => _minLines;
    2413             :   int? _minLines;
    2414             : 
    2415             :   /// The value may be null. If it is not null, then it must be greater than zero.
    2416           2 :   set minLines(int? value) {
    2417           1 :     assert(value == null || value > 0);
    2418           4 :     if (minLines == value) return;
    2419           0 :     _minLines = value;
    2420           0 :     markNeedsTextLayout();
    2421             :   }
    2422             : 
    2423             :   /// Whether this widget's width will be sized to fill its parent.
    2424             :   ///
    2425             :   /// If set to true and wrapped in a parent widget like [Expanded] or
    2426             :   /// [SizedBox], the input will expand to fill the parent.
    2427             :   ///
    2428             :   /// [maxLines] and [minLines] must both be null when this is set to true,
    2429             :   /// otherwise an error is thrown.
    2430             :   ///
    2431             :   /// Defaults to false.
    2432             :   ///
    2433             :   /// See the examples in [maxLines] for the complete picture of how [maxLines],
    2434             :   /// [minLines], and [expands] interact to produce various behaviors.
    2435             :   ///
    2436             :   /// Input that matches the width of its parent:
    2437             :   /// ```dart
    2438             :   /// Expanded(
    2439             :   ///   child: TextField(maxLines: null, expands: true),
    2440             :   /// )
    2441             :   /// ```
    2442           6 :   bool get expands => _expands;
    2443             :   bool _expands;
    2444           2 :   set expands(bool value) {
    2445           4 :     if (expands == value) return;
    2446           0 :     _expands = value;
    2447           0 :     markNeedsTextLayout();
    2448             :   }
    2449             : 
    2450             :   /// The color to use when painting the selection.
    2451           3 :   Color? get selectionColor => _selectionPainter.highlightColor;
    2452           2 :   set selectionColor(Color? value) {
    2453           4 :     _selectionPainter.highlightColor = value;
    2454             :   }
    2455             : 
    2456             :   /// The number of font pixels for each logical pixel.
    2457             :   ///
    2458             :   /// For example, if the text scale factor is 1.5, text will be 50% larger than
    2459             :   /// the specified font size.
    2460           3 :   double get textScaleFactor => _textPainter.textScaleFactor;
    2461           3 :   set textScaleFactor(double value) {
    2462           9 :     if (_textPainter.textScaleFactor == value) return;
    2463           2 :     _textPainter.textScaleFactor = value;
    2464           1 :     markNeedsTextLayout();
    2465             :   }
    2466             : 
    2467             :   /// The region of text that is selected, if any.
    2468             :   ///
    2469             :   /// The caret position is represented by a collapsed selection.
    2470             :   ///
    2471             :   /// If [selection] is null, there is no selection and attempts to
    2472             :   /// manipulate the selection will throw.
    2473           6 :   TextSelection? get selection => _selection;
    2474             :   TextSelection? _selection;
    2475           3 :   set selection(TextSelection? value) {
    2476           6 :     if (_selection == value) return;
    2477           3 :     _selection = value;
    2478           6 :     _selectionPainter.highlightedRange = value;
    2479           3 :     markNeedsPaint();
    2480           3 :     markNeedsSemanticsUpdate();
    2481             :   }
    2482             : 
    2483             :   /// The offset at which the text should be painted.
    2484             :   ///
    2485             :   /// If the text content is larger than the editable line itself, the editable
    2486             :   /// line clips the text. This property controls which part of the text is
    2487             :   /// visible by shifting the text by the given offset before clipping.
    2488           6 :   ViewportOffset get offset => _offset;
    2489             :   ViewportOffset _offset;
    2490           3 :   set offset(ViewportOffset value) {
    2491           6 :     if (_offset == value) return;
    2492           8 :     if (attached) _offset.removeListener(markNeedsPaint);
    2493           2 :     _offset = value;
    2494           8 :     if (attached) _offset.addListener(markNeedsPaint);
    2495           2 :     markNeedsLayout();
    2496             :   }
    2497             : 
    2498             :   /// How wide the cursor will be.
    2499             :   ///
    2500             :   /// This can be null, in which case the getter will actually return [preferredLineWidth].
    2501             :   ///
    2502             :   /// Setting this to itself fixes the value to the current [preferredLineWidth]. Setting
    2503             :   /// this to null returns the behavior of deferring to [preferredLineWidth].
    2504           9 :   double get cursorWidth => _cursorWidth ?? preferredLineWidth;
    2505             :   double? _cursorWidth;
    2506           2 :   set cursorWidth(double? value) {
    2507           4 :     if (_cursorWidth == value) return;
    2508           0 :     _cursorWidth = value;
    2509           0 :     markNeedsLayout();
    2510             :   }
    2511             : 
    2512             :   /// How thick the cursor will be.
    2513             :   ///
    2514             :   /// The cursor will draw over the text. The cursor height will extend
    2515             :   /// down between the boundary of characters. This corresponds to extending
    2516             :   /// downstream relative to the selected position. Negative values may be used
    2517             :   /// to reverse this behavior.
    2518           6 :   double get cursorHeight => _cursorHeight;
    2519             :   double _cursorHeight = 1.0;
    2520           3 :   set cursorHeight(double value) {
    2521           6 :     if (_cursorHeight == value) {
    2522             :       return;
    2523             :     }
    2524           2 :     _cursorHeight = value;
    2525           2 :     markNeedsLayout();
    2526             :   }
    2527             : 
    2528             :   /// The offset that is used, in pixels, when painting the cursor on screen.
    2529             :   ///
    2530             :   /// By default, the cursor position should be set to an offset of
    2531             :   /// (0.0, -[cursorHeight] * 0.5) on iOS platforms and (0, 0) on Android
    2532             :   /// platforms. The origin from where the offset is applied to is the arbitrary
    2533             :   /// location where the cursor ends up being rendered from by default.
    2534           6 :   Offset get cursorOffset => _caretPainter.cursorOffset;
    2535           2 :   set cursorOffset(Offset value) {
    2536           4 :     _caretPainter.cursorOffset = value;
    2537             :   }
    2538             : 
    2539             :   /// How rounded the corners of the cursor should be.
    2540             :   ///
    2541             :   /// A null value is the same as [Radius.zero].
    2542           3 :   Radius? get cursorRadius => _caretPainter.cursorRadius;
    2543           3 :   set cursorRadius(Radius? value) {
    2544           6 :     _caretPainter.cursorRadius = value;
    2545             :   }
    2546             : 
    2547             :   /// The [LayerLink] of start selection handle.
    2548             :   ///
    2549             :   /// [MongolRenderEditable] is responsible for calculating the [Offset] of this
    2550             :   /// [LayerLink], which will be used as [CompositedTransformTarget] of start handle.
    2551           6 :   LayerLink get startHandleLayerLink => _startHandleLayerLink;
    2552             :   LayerLink _startHandleLayerLink;
    2553           2 :   set startHandleLayerLink(LayerLink value) {
    2554           4 :     if (_startHandleLayerLink == value) return;
    2555           0 :     _startHandleLayerLink = value;
    2556           0 :     markNeedsPaint();
    2557             :   }
    2558             : 
    2559             :   /// The [LayerLink] of end selection handle.
    2560             :   ///
    2561             :   /// [MongolRenderEditable] is responsible for calculating the [Offset] of this
    2562             :   /// [LayerLink], which will be used as [CompositedTransformTarget] of end handle.
    2563           6 :   LayerLink get endHandleLayerLink => _endHandleLayerLink;
    2564             :   LayerLink _endHandleLayerLink;
    2565           2 :   set endHandleLayerLink(LayerLink value) {
    2566           4 :     if (_endHandleLayerLink == value) return;
    2567           0 :     _endHandleLayerLink = value;
    2568           0 :     markNeedsPaint();
    2569             :   }
    2570             : 
    2571             :   /// Whether to allow the user to change the selection.
    2572             :   ///
    2573             :   /// Since [MongolRenderEditable] does not handle selection manipulation
    2574             :   /// itself, this actually only affects whether the accessibility
    2575             :   /// hints provided to the system (via
    2576             :   /// [describeSemanticsConfiguration]) will enable selection
    2577             :   /// manipulation. It's the responsibility of this object's owner
    2578             :   /// to provide selection manipulation affordances.
    2579             :   ///
    2580             :   /// This field is used by [selectionEnabled] (which then controls
    2581             :   /// the accessibility hints mentioned above). When null,
    2582             :   /// [obscureText] is used to determine the value of
    2583             :   /// [selectionEnabled] instead.
    2584           6 :   bool? get enableInteractiveSelection => _enableInteractiveSelection;
    2585             :   bool? _enableInteractiveSelection;
    2586           0 :   set enableInteractiveSelection(bool? value) {
    2587           0 :     if (_enableInteractiveSelection == value) return;
    2588           0 :     _enableInteractiveSelection = value;
    2589           0 :     markNeedsTextLayout();
    2590           0 :     markNeedsSemanticsUpdate();
    2591             :   }
    2592             : 
    2593             :   /// Whether interactive selection is enabled based on the values of
    2594             :   /// [enableInteractiveSelection] and [obscureText].
    2595             :   ///
    2596             :   /// Since [MongolRenderEditable] does not handle selection manipulation
    2597             :   /// itself, this actually only affects whether the accessibility
    2598             :   /// hints provided to the system (via
    2599             :   /// [describeSemanticsConfiguration]) will enable selection
    2600             :   /// manipulation. It's the responsibility of this object's owner
    2601             :   /// to provide selection manipulation affordances.
    2602             :   ///
    2603             :   /// By default, [enableInteractiveSelection] is null, [obscureText] is false,
    2604             :   /// and this getter returns true.
    2605             :   ///
    2606             :   /// If [enableInteractiveSelection] is null and [obscureText] is true, then this
    2607             :   /// getter returns false. This is the common case for password fields.
    2608             :   ///
    2609             :   /// If [enableInteractiveSelection] is non-null then its value is
    2610             :   /// returned. An application might [enableInteractiveSelection] to
    2611             :   /// true to enable interactive selection for a password field, or to
    2612             :   /// false to unconditionally disable interactive selection.
    2613           3 :   bool get selectionEnabled {
    2614           4 :     return enableInteractiveSelection ?? !obscureText;
    2615             :   }
    2616             : 
    2617             :   /// The maximum amount the text is allowed to scroll.
    2618             :   ///
    2619             :   /// This value is only valid after layout and can change as additional
    2620             :   /// text is entered or removed in order to accommodate expanding when
    2621             :   /// [expands] is set to true.
    2622           4 :   double get maxScrollExtent => _maxScrollExtent;
    2623             :   double _maxScrollExtent = 0;
    2624             : 
    2625           9 :   double get _caretMargin => _kCaretGap + cursorHeight;
    2626             : 
    2627             :   /// Defaults to [Clip.hardEdge], and must not be null.
    2628           6 :   Clip get clipBehavior => _clipBehavior;
    2629             :   Clip _clipBehavior = Clip.hardEdge;
    2630           2 :   set clipBehavior(Clip value) {
    2631           4 :     if (value != _clipBehavior) {
    2632           1 :       _clipBehavior = value;
    2633           1 :       markNeedsPaint();
    2634           1 :       markNeedsSemanticsUpdate();
    2635             :     }
    2636             :   }
    2637             : 
    2638             :   /// Collected during [describeSemanticsConfiguration], used by
    2639             :   /// [assembleSemanticsNode] and [_combineSemanticsInfo].
    2640             :   List<InlineSpanSemanticsInformation>? _semanticsInfo;
    2641             : 
    2642             :   // Caches [SemanticsNode]s created during [assembleSemanticsNode] so they
    2643             :   // can be re-used when [assembleSemanticsNode] is called again. This ensures
    2644             :   // stable ids for the [SemanticsNode]s of [TextSpan]s across
    2645             :   // [assembleSemanticsNode] invocations.
    2646             :   Queue<SemanticsNode>? _cachedChildNodes;
    2647             : 
    2648           3 :   @override
    2649             :   void describeSemanticsConfiguration(SemanticsConfiguration config) {
    2650           3 :     super.describeSemanticsConfiguration(config);
    2651          12 :     _semanticsInfo = _textPainter.text!.getSemanticsInformation();
    2652             :     // TODO(chunhtai): the macOS does not provide a public API to support text
    2653             :     // selections across multiple semantics nodes. Remove this platform check
    2654             :     // once we can support it.
    2655             :     // https://github.com/flutter/flutter/issues/77957
    2656           6 :     if (_semanticsInfo!.any(
    2657           6 :             (InlineSpanSemanticsInformation info) => info.recognizer != null) &&
    2658           0 :         defaultTargetPlatform != TargetPlatform.macOS) {
    2659           0 :       assert(readOnly && !obscureText);
    2660             :       // For Selectable rich text with recognizer, we need to create a semantics
    2661             :       // node for each text fragment.
    2662             :       config
    2663           0 :         ..isSemanticBoundary = true
    2664           0 :         ..explicitChildNodes = true;
    2665             :       return;
    2666             :     }
    2667             :     config
    2668           3 :       ..value =
    2669          18 :           obscureText ? obscuringCharacter * _plainText.length : _plainText
    2670           6 :       ..isObscured = obscureText
    2671           6 :       ..isMultiline = _isMultiline
    2672           3 :       ..textDirection = TextDirection.ltr
    2673           6 :       ..isFocused = hasFocus
    2674           3 :       ..isTextField = true
    2675           6 :       ..isReadOnly = readOnly;
    2676             : 
    2677           5 :     if (hasFocus && selectionEnabled) {
    2678           4 :       config.onSetSelection = _handleSetSelection;
    2679             :     }
    2680             : 
    2681           9 :     if (hasFocus && !readOnly) config.onSetText = _handleSetText;
    2682             : 
    2683          12 :     if (selectionEnabled && selection?.isValid == true) {
    2684           6 :       config.textSelection = selection;
    2685          12 :       if (_textPainter.getOffsetBefore(selection!.extentOffset) != null) {
    2686             :         config
    2687           6 :           ..onMoveCursorBackwardByWord = _handleMoveCursorBackwardByWord
    2688           3 :           ..onMoveCursorBackwardByCharacter =
    2689           3 :               _handleMoveCursorBackwardByCharacter;
    2690             :       }
    2691          12 :       if (_textPainter.getOffsetAfter(selection!.extentOffset) != null) {
    2692             :         config
    2693           6 :           ..onMoveCursorForwardByWord = _handleMoveCursorForwardByWord
    2694           3 :           ..onMoveCursorForwardByCharacter =
    2695           3 :               _handleMoveCursorForwardByCharacter;
    2696             :       }
    2697             :     }
    2698             :   }
    2699             : 
    2700           0 :   void _handleSetText(String text) {
    2701           0 :     textSelectionDelegate.userUpdateTextEditingValue(
    2702           0 :       TextEditingValue(
    2703             :         text: text,
    2704           0 :         selection: TextSelection.collapsed(offset: text.length),
    2705             :       ),
    2706             :       SelectionChangedCause.keyboard,
    2707             :     );
    2708             :   }
    2709             : 
    2710           0 :   @override
    2711             :   void assembleSemanticsNode(SemanticsNode node, SemanticsConfiguration config,
    2712             :       Iterable<SemanticsNode> children) {
    2713           0 :     assert(_semanticsInfo != null && _semanticsInfo!.isNotEmpty);
    2714           0 :     final newChildren = <SemanticsNode>[];
    2715             :     Rect currentRect;
    2716             :     var ordinal = 0.0;
    2717             :     var start = 0;
    2718           0 :     final newChildCache = Queue<SemanticsNode>();
    2719           0 :     for (final info in combineSemanticsInfo(_semanticsInfo!)) {
    2720           0 :       assert(!info.isPlaceholder);
    2721           0 :       final selection = TextSelection(
    2722             :         baseOffset: start,
    2723           0 :         extentOffset: start + info.text.length,
    2724             :       );
    2725           0 :       start += info.text.length;
    2726             : 
    2727           0 :       final rects = _textPainter.getBoxesForSelection(selection);
    2728           0 :       if (rects.isEmpty) {
    2729             :         continue;
    2730             :       }
    2731           0 :       var rect = rects.first;
    2732           0 :       for (final textBox in rects.skip(1)) {
    2733           0 :         rect = rect.expandToInclude(textBox);
    2734             :       }
    2735             :       // Any of the text boxes may have had infinite dimensions.
    2736             :       // We shouldn't pass infinite dimensions up to the bridges.
    2737           0 :       rect = Rect.fromLTWH(
    2738           0 :         math.max(0.0, rect.left),
    2739           0 :         math.max(0.0, rect.top),
    2740           0 :         math.min(rect.width, constraints.maxWidth),
    2741           0 :         math.min(rect.height, constraints.maxHeight),
    2742             :       );
    2743             :       // Round the current rectangle to make this API testable and add some
    2744             :       // padding so that the accessibility rects do not overlap with the text.
    2745           0 :       currentRect = Rect.fromLTRB(
    2746           0 :         rect.left.floorToDouble() - 4.0,
    2747           0 :         rect.top.floorToDouble() - 4.0,
    2748           0 :         rect.right.ceilToDouble() + 4.0,
    2749           0 :         rect.bottom.ceilToDouble() + 4.0,
    2750             :       );
    2751           0 :       final configuration = SemanticsConfiguration()
    2752           0 :         ..sortKey = OrdinalSortKey(ordinal++)
    2753           0 :         ..textDirection = TextDirection.ltr
    2754           0 :         ..label = info.semanticsLabel ?? info.text;
    2755           0 :       final recognizer = info.recognizer;
    2756             :       if (recognizer != null) {
    2757           0 :         if (recognizer is TapGestureRecognizer) {
    2758           0 :           if (recognizer.onTap != null) {
    2759           0 :             configuration.onTap = recognizer.onTap;
    2760           0 :             configuration.isLink = true;
    2761             :           }
    2762           0 :         } else if (recognizer is DoubleTapGestureRecognizer) {
    2763           0 :           if (recognizer.onDoubleTap != null) {
    2764           0 :             configuration.onTap = recognizer.onDoubleTap;
    2765           0 :             configuration.isLink = true;
    2766             :           }
    2767           0 :         } else if (recognizer is LongPressGestureRecognizer) {
    2768           0 :           if (recognizer.onLongPress != null) {
    2769           0 :             configuration.onLongPress = recognizer.onLongPress;
    2770             :           }
    2771             :         } else {
    2772           0 :           assert(false, '${recognizer.runtimeType} is not supported.');
    2773             :         }
    2774             :       }
    2775           0 :       final newChild = (_cachedChildNodes?.isNotEmpty == true)
    2776           0 :           ? _cachedChildNodes!.removeFirst()
    2777           0 :           : SemanticsNode();
    2778             :       newChild
    2779           0 :         ..updateWith(config: configuration)
    2780           0 :         ..rect = currentRect;
    2781           0 :       newChildCache.addLast(newChild);
    2782           0 :       newChildren.add(newChild);
    2783             :     }
    2784           0 :     _cachedChildNodes = newChildCache;
    2785           0 :     node.updateWith(config: config, childrenInInversePaintOrder: newChildren);
    2786             :   }
    2787             : 
    2788           0 :   void _handleSetSelection(TextSelection selection) {
    2789           0 :     _handleSelectionChange(selection, SelectionChangedCause.keyboard);
    2790             :   }
    2791             : 
    2792           0 :   void _handleMoveCursorForwardByCharacter(bool extentSelection) {
    2793           0 :     assert(selection != null);
    2794           0 :     final extentOffset = _textPainter.getOffsetAfter(selection!.extentOffset);
    2795             :     if (extentOffset == null) return;
    2796           0 :     final baseOffset = !extentSelection ? extentOffset : selection!.baseOffset;
    2797           0 :     _handleSelectionChange(
    2798           0 :       TextSelection(baseOffset: baseOffset, extentOffset: extentOffset),
    2799             :       SelectionChangedCause.keyboard,
    2800             :     );
    2801             :   }
    2802             : 
    2803           0 :   void _handleMoveCursorBackwardByCharacter(bool extentSelection) {
    2804           0 :     assert(selection != null);
    2805           0 :     final extentOffset = _textPainter.getOffsetBefore(selection!.extentOffset);
    2806             :     if (extentOffset == null) return;
    2807           0 :     final baseOffset = !extentSelection ? extentOffset : selection!.baseOffset;
    2808           0 :     _handleSelectionChange(
    2809           0 :       TextSelection(baseOffset: baseOffset, extentOffset: extentOffset),
    2810             :       SelectionChangedCause.keyboard,
    2811             :     );
    2812             :   }
    2813             : 
    2814           0 :   void _handleMoveCursorForwardByWord(bool extentSelection) {
    2815           0 :     assert(selection != null);
    2816           0 :     final currentWord = _textPainter.getWordBoundary(selection!.extent);
    2817           0 :     final nextWord = _getNextWord(currentWord.end);
    2818             :     if (nextWord == null) return;
    2819           0 :     final baseOffset = extentSelection ? selection!.baseOffset : nextWord.start;
    2820           0 :     _handleSelectionChange(
    2821           0 :       TextSelection(
    2822             :         baseOffset: baseOffset,
    2823           0 :         extentOffset: nextWord.start,
    2824             :       ),
    2825             :       SelectionChangedCause.keyboard,
    2826             :     );
    2827             :   }
    2828             : 
    2829           0 :   void _handleMoveCursorBackwardByWord(bool extentSelection) {
    2830           0 :     assert(selection != null);
    2831           0 :     final currentWord = _textPainter.getWordBoundary(selection!.extent);
    2832           0 :     final previousWord = _getPreviousWord(currentWord.start - 1);
    2833             :     if (previousWord == null) return;
    2834             :     final baseOffset =
    2835           0 :         extentSelection ? selection!.baseOffset : previousWord.start;
    2836           0 :     _handleSelectionChange(
    2837           0 :       TextSelection(
    2838             :         baseOffset: baseOffset,
    2839           0 :         extentOffset: previousWord.start,
    2840             :       ),
    2841             :       SelectionChangedCause.keyboard,
    2842             :     );
    2843             :   }
    2844             : 
    2845           1 :   TextRange? _getNextWord(int offset) {
    2846             :     while (true) {
    2847           3 :       final range = _textPainter.getWordBoundary(TextPosition(offset: offset));
    2848           2 :       if (!range.isValid || range.isCollapsed) return null;
    2849           1 :       if (!_onlyWhitespace(range)) return range;
    2850           1 :       offset = range.end;
    2851             :     }
    2852             :   }
    2853             : 
    2854           1 :   TextRange? _getPreviousWord(int offset) {
    2855           1 :     while (offset >= 0) {
    2856           3 :       final range = _textPainter.getWordBoundary(TextPosition(offset: offset));
    2857           2 :       if (!range.isValid || range.isCollapsed) return null;
    2858           1 :       if (!_onlyWhitespace(range)) return range;
    2859           2 :       offset = range.start - 1;
    2860             :     }
    2861             :     return null;
    2862             :   }
    2863             : 
    2864             :   // Check if the given text range only contains white space or separator
    2865             :   // characters.
    2866             :   //
    2867             :   // Includes newline characters from ASCII and separators from the
    2868             :   // [unicode separator category](https://www.compart.com/en/unicode/category/Zs)
    2869           1 :   bool _onlyWhitespace(TextRange range) {
    2870           4 :     for (var i = range.start; i < range.end; i++) {
    2871           2 :       final codeUnit = text!.codeUnitAt(i)!;
    2872           1 :       if (!_isWhitespace(codeUnit)) {
    2873             :         return false;
    2874             :       }
    2875             :     }
    2876             :     return true;
    2877             :   }
    2878             : 
    2879           3 :   @override
    2880             :   void attach(PipelineOwner owner) {
    2881           3 :     super.attach(owner);
    2882           6 :     _foregroundRenderObject?.attach(owner);
    2883           6 :     _backgroundRenderObject?.attach(owner);
    2884             : 
    2885           6 :     _tap = TapGestureRecognizer(debugOwner: this)
    2886           6 :       ..onTapDown = _handleTapDown
    2887           6 :       ..onTap = _handleTap;
    2888           6 :     _longPress = LongPressGestureRecognizer(debugOwner: this)
    2889           6 :       ..onLongPress = _handleLongPress;
    2890           9 :     _offset.addListener(markNeedsPaint);
    2891           3 :     _showHideCursor();
    2892           9 :     _showCursor.addListener(_showHideCursor);
    2893           3 :     assert(!_listenerAttached);
    2894           3 :     if (_hasFocus) {
    2895           0 :       RawKeyboard.instance.addListener(_handleKeyEvent);
    2896           0 :       _listenerAttached = true;
    2897             :     }
    2898             :   }
    2899             : 
    2900           3 :   @override
    2901             :   void detach() {
    2902           6 :     _tap.dispose();
    2903           6 :     _longPress.dispose();
    2904           9 :     _offset.removeListener(markNeedsPaint);
    2905           9 :     _showCursor.removeListener(_showHideCursor);
    2906             :     // TODO(justinmc): This listener should be ported to Actions and removed.
    2907             :     // https://github.com/flutter/flutter/issues/75004
    2908           3 :     if (_listenerAttached) {
    2909           9 :       RawKeyboard.instance.removeListener(_handleKeyEvent);
    2910           3 :       _listenerAttached = false;
    2911             :     }
    2912           3 :     super.detach();
    2913           6 :     _foregroundRenderObject?.detach();
    2914           6 :     _backgroundRenderObject?.detach();
    2915             :   }
    2916             : 
    2917           3 :   @override
    2918             :   void redepthChildren() {
    2919           3 :     final RenderObject? foregroundChild = _foregroundRenderObject;
    2920           3 :     final RenderObject? backgroundChild = _backgroundRenderObject;
    2921           3 :     if (foregroundChild != null) redepthChild(foregroundChild);
    2922           3 :     if (backgroundChild != null) redepthChild(backgroundChild);
    2923             :   }
    2924             : 
    2925           3 :   @override
    2926             :   void visitChildren(RenderObjectVisitor visitor) {
    2927           3 :     final RenderObject? foregroundChild = _foregroundRenderObject;
    2928           3 :     final RenderObject? backgroundChild = _backgroundRenderObject;
    2929           3 :     if (foregroundChild != null) visitor(foregroundChild);
    2930           3 :     if (backgroundChild != null) visitor(backgroundChild);
    2931             :   }
    2932             : 
    2933           9 :   bool get _isMultiline => maxLines != 1;
    2934             : 
    2935           6 :   Axis get _viewportAxis => _isMultiline ? Axis.horizontal : Axis.vertical;
    2936             : 
    2937           3 :   Offset get _paintOffset {
    2938           3 :     switch (_viewportAxis) {
    2939           3 :       case Axis.horizontal:
    2940          12 :         return Offset(-offset.pixels, 0.0);
    2941           3 :       case Axis.vertical:
    2942          12 :         return Offset(0.0, -offset.pixels);
    2943             :     }
    2944             :   }
    2945             : 
    2946           3 :   double get _viewportExtent {
    2947           3 :     assert(hasSize);
    2948           3 :     switch (_viewportAxis) {
    2949           3 :       case Axis.horizontal:
    2950           6 :         return size.width;
    2951           3 :       case Axis.vertical:
    2952           6 :         return size.height;
    2953             :     }
    2954             :   }
    2955             : 
    2956           3 :   double _getMaxScrollExtent(Size contentSize) {
    2957           3 :     assert(hasSize);
    2958           3 :     switch (_viewportAxis) {
    2959           3 :       case Axis.horizontal:
    2960          15 :         return math.max(0.0, contentSize.width - size.width);
    2961           3 :       case Axis.vertical:
    2962          15 :         return math.max(0.0, contentSize.height - size.height);
    2963             :     }
    2964             :   }
    2965             : 
    2966             :   // We need to check the paint offset here because during animation, the start of
    2967             :   // the text may position outside the visible region even when the text fits.
    2968           3 :   bool get _hasVisualOverflow =>
    2969          12 :       _maxScrollExtent > 0 || _paintOffset != Offset.zero;
    2970             : 
    2971             :   /// Returns the local coordinates of the endpoints of the given selection.
    2972             :   ///
    2973             :   /// If the selection is collapsed (and therefore occupies a single point), the
    2974             :   /// returned list is of length one. Otherwise, the selection is not collapsed
    2975             :   /// and the returned list is of length two.
    2976             :   ///
    2977             :   /// See also:
    2978             :   ///
    2979             :   ///  * [getLocalRectForCaret], which is the equivalent but for
    2980             :   ///    a [TextPosition] rather than a [TextSelection].
    2981           3 :   List<TextSelectionPoint> getEndpointsForSelection(TextSelection selection) {
    2982           3 :     _layoutText(
    2983          12 :         minHeight: constraints.minHeight, maxHeight: constraints.maxHeight);
    2984             : 
    2985           3 :     final paintOffset = _paintOffset;
    2986             : 
    2987           3 :     final boxes = selection.isCollapsed
    2988           3 :         ? <Rect>[]
    2989           6 :         : _textPainter.getBoxesForSelection(selection);
    2990           3 :     if (boxes.isEmpty) {
    2991             :       final caretOffset =
    2992          12 :           _textPainter.getOffsetForCaret(selection.extent, _caretPrototype);
    2993          12 :       final start = Offset(preferredLineWidth, 0.0) + caretOffset + paintOffset;
    2994           6 :       return <TextSelectionPoint>[TextSelectionPoint(start, TextDirection.ltr)];
    2995             :     } else {
    2996          18 :       final start = Offset(boxes.first.left, boxes.first.top) + paintOffset;
    2997          18 :       final end = Offset(boxes.last.right, boxes.last.bottom) + paintOffset;
    2998           3 :       return <TextSelectionPoint>[
    2999           3 :         TextSelectionPoint(start, TextDirection.ltr),
    3000           3 :         TextSelectionPoint(end, TextDirection.ltr),
    3001             :       ];
    3002             :     }
    3003             :   }
    3004             : 
    3005             :   /// Returns the smallest [Rect], in the local coordinate system, that covers
    3006             :   /// the text within the [TextRange] specified.
    3007             :   ///
    3008             :   /// This method is used to calculate the approximate position of the IME bar
    3009             :   /// on iOS.
    3010             :   ///
    3011             :   /// Returns null if [TextRange.isValid] is false for the given `range`, or the
    3012             :   /// given `range` is collapsed.
    3013           3 :   Rect? getRectForComposingRange(TextRange range) {
    3014           6 :     if (!range.isValid || range.isCollapsed) {
    3015             :       return null;
    3016             :     }
    3017           3 :     _layoutText(
    3018          12 :         minHeight: constraints.minHeight, maxHeight: constraints.maxHeight);
    3019             : 
    3020           6 :     final boxes = _textPainter.getBoxesForSelection(
    3021           9 :       TextSelection(baseOffset: range.start, extentOffset: range.end),
    3022             :     );
    3023             : 
    3024             :     return boxes
    3025           3 :         .fold(
    3026             :           null,
    3027           3 :           (Rect? accum, Rect incoming) =>
    3028           0 :               accum?.expandToInclude(incoming) ?? incoming,
    3029             :         )
    3030           6 :         ?.shift(_paintOffset);
    3031             :   }
    3032             : 
    3033             :   /// Returns the position in the text for the given global coordinate.
    3034             :   ///
    3035             :   /// See also:
    3036             :   ///
    3037             :   ///  * [getLocalRectForCaret], which is the reverse operation, taking
    3038             :   ///    a [TextPosition] and returning a [Rect].
    3039             :   ///  * [MongolTextPainter.getPositionForOffset], which is the equivalent method
    3040             :   ///    for a [MongolTextPainter] object.
    3041           0 :   TextPosition getPositionForPoint(Offset globalPosition) {
    3042           0 :     _layoutText(
    3043           0 :         minHeight: constraints.minHeight, maxHeight: constraints.maxHeight);
    3044           0 :     globalPosition += -_paintOffset;
    3045           0 :     return _textPainter.getPositionForOffset(globalToLocal(globalPosition));
    3046             :   }
    3047             : 
    3048             :   /// Returns the [Rect] in local coordinates for the caret at the given text
    3049             :   /// position.
    3050             :   ///
    3051             :   /// See also:
    3052             :   ///
    3053             :   ///  * [getPositionForPoint], which is the reverse operation, taking
    3054             :   ///    an [Offset] in global coordinates and returning a [TextPosition].
    3055             :   ///  * [getEndpointsForSelection], which is the equivalent but for
    3056             :   ///    a selection rather than a particular text position.
    3057             :   ///  * [MongolTextPainter.getOffsetForCaret], the equivalent method for a
    3058             :   ///    [MongolTextPainter] object.
    3059           2 :   Rect getLocalRectForCaret(TextPosition caretPosition) {
    3060           2 :     _layoutText(
    3061           8 :         minHeight: constraints.minHeight, maxHeight: constraints.maxHeight);
    3062             :     final caretOffset =
    3063           6 :         _textPainter.getOffsetForCaret(caretPosition, _caretPrototype);
    3064             :     // This rect is the same as _caretPrototype but without the horizontal padding.
    3065           6 :     final rect = Rect.fromLTWH(0.0, 0.0, cursorWidth, cursorHeight)
    3066          10 :         .shift(caretOffset + _paintOffset + cursorOffset);
    3067             :     // Add additional cursor offset (generally only if on iOS).
    3068           6 :     return rect.shift(_snapToPhysicalPixel(rect.topLeft));
    3069             :   }
    3070             : 
    3071           3 :   @override
    3072             :   double computeMinIntrinsicHeight(double width) {
    3073           3 :     _layoutText(maxHeight: double.infinity);
    3074           6 :     return _textPainter.minIntrinsicHeight;
    3075             :   }
    3076             : 
    3077           3 :   @override
    3078             :   double computeMaxIntrinsicHeight(double width) {
    3079           3 :     _layoutText(maxHeight: double.infinity);
    3080          12 :     return _textPainter.maxIntrinsicHeight + cursorHeight;
    3081             :   }
    3082             : 
    3083             :   /// An estimate of the width of a line in the text. See [TextPainter.preferredLineWidth].
    3084             :   /// This does not require the layout to be updated.
    3085           9 :   double get preferredLineWidth => _textPainter.preferredLineWidth;
    3086             : 
    3087           3 :   double _preferredWidth(double height) {
    3088             :     // Lock width to maxLines if needed.
    3089           6 :     final lockedMax = maxLines != null && minLines == null;
    3090           6 :     final lockedBoth = minLines != null && minLines == maxLines;
    3091           6 :     final singleLine = maxLines == 1;
    3092             :     if (singleLine || lockedMax || lockedBoth) {
    3093           9 :       return preferredLineWidth * maxLines!;
    3094             :     }
    3095             : 
    3096             :     // Clamp width to minLines or maxLines if needed.
    3097           4 :     final minLimited = minLines != null && minLines! > 1;
    3098           2 :     final maxLimited = maxLines != null;
    3099             :     if (minLimited || maxLimited) {
    3100           1 :       _layoutText(maxHeight: height);
    3101           6 :       if (minLimited && _textPainter.width < preferredLineWidth * minLines!) {
    3102           3 :         return preferredLineWidth * minLines!;
    3103             :       }
    3104           0 :       if (maxLimited && _textPainter.width > preferredLineWidth * maxLines!) {
    3105           0 :         return preferredLineWidth * maxLines!;
    3106             :       }
    3107             :     }
    3108             : 
    3109             :     // Set the width based on the content.
    3110           2 :     if (height == double.infinity) {
    3111           1 :       final text = _plainText;
    3112             :       var lines = 1;
    3113           3 :       for (var index = 0; index < text.length; index += 1) {
    3114             :         const newline = 0x0A;
    3115           2 :         if (text.codeUnitAt(index) == newline) {
    3116           1 :           lines += 1;
    3117             :         }
    3118             :       }
    3119           2 :       return preferredLineWidth * lines;
    3120             :     }
    3121           2 :     _layoutText(maxHeight: height);
    3122           8 :     return math.max(preferredLineWidth, _textPainter.width);
    3123             :   }
    3124             : 
    3125           3 :   @override
    3126             :   double computeMinIntrinsicWidth(double height) {
    3127           3 :     return _preferredWidth(height);
    3128             :   }
    3129             : 
    3130           3 :   @override
    3131             :   double computeMaxIntrinsicWidth(double height) {
    3132           3 :     return _preferredWidth(height);
    3133             :   }
    3134             : 
    3135           1 :   @override
    3136             :   double computeDistanceToActualBaseline(TextBaseline baseline) {
    3137           1 :     _layoutText(
    3138           4 :         minHeight: constraints.minHeight, maxHeight: constraints.maxHeight);
    3139           2 :     return _textPainter.computeDistanceToActualBaseline(baseline);
    3140             :   }
    3141             : 
    3142           2 :   @override
    3143             :   bool hitTestSelf(Offset position) => true;
    3144             : 
    3145             :   late TapGestureRecognizer _tap;
    3146             :   late LongPressGestureRecognizer _longPress;
    3147             : 
    3148           2 :   @override
    3149             :   void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
    3150           2 :     assert(debugHandleEvent(event, entry));
    3151           2 :     if (event is PointerDownEvent) {
    3152           2 :       assert(!debugNeedsLayout);
    3153             : 
    3154           2 :       if (!ignorePointer) {
    3155             :         // Propagates the pointer event to selection handlers.
    3156           2 :         _tap.addPointer(event);
    3157           2 :         _longPress.addPointer(event);
    3158             :       }
    3159             :     }
    3160             :   }
    3161             : 
    3162             :   Offset? _lastTapDownPosition;
    3163             :   Offset? _lastSecondaryTapDownPosition;
    3164             : 
    3165             :   /// The position of the most recent secondary tap down event on this text
    3166             :   /// input.
    3167           4 :   Offset? get lastSecondaryTapDownPosition => _lastSecondaryTapDownPosition;
    3168             : 
    3169             :   /// Tracks the position of a secondary tap event.
    3170             :   ///
    3171             :   /// Should be called before attempting to change the selection based on the
    3172             :   /// position of a secondary tap.
    3173           0 :   void handleSecondaryTapDown(TapDownDetails details) {
    3174           0 :     _lastTapDownPosition = details.globalPosition;
    3175           0 :     _lastSecondaryTapDownPosition = details.globalPosition;
    3176             :   }
    3177             : 
    3178             :   /// If [ignorePointer] is false (the default) then this method is called by
    3179             :   /// the internal gesture recognizer's [TapGestureRecognizer.onTapDown]
    3180             :   /// callback.
    3181             :   ///
    3182             :   /// When [ignorePointer] is true, an ancestor widget must respond to tap
    3183             :   /// down events by calling this method.
    3184           3 :   void handleTapDown(TapDownDetails details) {
    3185           6 :     _lastTapDownPosition = details.globalPosition;
    3186             :   }
    3187             : 
    3188           1 :   void _handleTapDown(TapDownDetails details) {
    3189           1 :     assert(!ignorePointer);
    3190           1 :     handleTapDown(details);
    3191             :   }
    3192             : 
    3193             :   /// If [ignorePointer] is false (the default) then this method is called by
    3194             :   /// the internal gesture recognizer's [TapGestureRecognizer.onTap]
    3195             :   /// callback.
    3196             :   ///
    3197             :   /// When [ignorePointer] is true, an ancestor widget must respond to tap
    3198             :   /// events by calling this method.
    3199           1 :   void handleTap() {
    3200           1 :     selectPosition(cause: SelectionChangedCause.tap);
    3201             :   }
    3202             : 
    3203           1 :   void _handleTap() {
    3204           1 :     assert(!ignorePointer);
    3205           1 :     handleTap();
    3206             :   }
    3207             : 
    3208             :   /// If [ignorePointer] is false (the default) then this method is called by
    3209             :   /// the internal gesture recognizer's [DoubleTapGestureRecognizer.onDoubleTap]
    3210             :   /// callback.
    3211             :   ///
    3212             :   /// When [ignorePointer] is true, an ancestor widget must respond to double
    3213             :   /// tap events by calling this method.
    3214           0 :   void handleDoubleTap() {
    3215           0 :     selectWord(cause: SelectionChangedCause.doubleTap);
    3216             :   }
    3217             : 
    3218             :   /// If [ignorePointer] is false (the default) then this method is called by
    3219             :   /// the internal gesture recognizer's [LongPressGestureRecognizer.onLongPress]
    3220             :   /// callback.
    3221             :   ///
    3222             :   /// When [ignorePointer] is true, an ancestor widget must respond to long
    3223             :   /// press events by calling this method.
    3224           0 :   void handleLongPress() {
    3225           0 :     selectWord(cause: SelectionChangedCause.longPress);
    3226             :   }
    3227             : 
    3228           0 :   void _handleLongPress() {
    3229           0 :     assert(!ignorePointer);
    3230           0 :     handleLongPress();
    3231             :   }
    3232             : 
    3233             :   /// Move selection to the location of the last tap down.
    3234             :   ///
    3235             :   /// This method is mainly used to translate user inputs in global positions
    3236             :   /// into a [TextSelection]. When used in conjunction with a [MongolEditableText],
    3237             :   /// the selection change is fed back into [TextEditingController.selection].
    3238             :   ///
    3239             :   /// If you have a [TextEditingController], it's generally easier to
    3240             :   /// programmatically manipulate its `value` or `selection` directly.
    3241           3 :   void selectPosition({required SelectionChangedCause cause}) {
    3242           6 :     selectPositionAt(from: _lastTapDownPosition!, cause: cause);
    3243             :   }
    3244             : 
    3245             :   /// Select text between the global positions [from] and [to].
    3246             :   ///
    3247             :   /// [from] corresponds to the [TextSelection.baseOffset], and [to] corresponds
    3248             :   /// to the [TextSelection.extentOffset].
    3249           3 :   void selectPositionAt(
    3250             :       {required Offset from,
    3251             :       Offset? to,
    3252             :       required SelectionChangedCause cause}) {
    3253           3 :     _layoutText(
    3254          12 :         minHeight: constraints.minHeight, maxHeight: constraints.maxHeight);
    3255             :     final fromPosition =
    3256          15 :         _textPainter.getPositionForOffset(globalToLocal(from - _paintOffset));
    3257             :     final toPosition = (to == null)
    3258             :         ? null
    3259           5 :         : _textPainter.getPositionForOffset(globalToLocal(to - _paintOffset));
    3260             : 
    3261           3 :     final baseOffset = fromPosition.offset;
    3262           4 :     final extentOffset = toPosition?.offset ?? fromPosition.offset;
    3263             : 
    3264           3 :     final newSelection = TextSelection(
    3265             :       baseOffset: baseOffset,
    3266             :       extentOffset: extentOffset,
    3267           3 :       affinity: fromPosition.affinity,
    3268             :     );
    3269           3 :     _setSelection(newSelection, cause);
    3270             :   }
    3271             : 
    3272             :   /// Select a word around the location of the last tap down.
    3273           2 :   void selectWord({required SelectionChangedCause cause}) {
    3274           4 :     selectWordsInRange(from: _lastTapDownPosition!, cause: cause);
    3275             :   }
    3276             : 
    3277             :   /// Selects the set words of a paragraph in a given range of global positions.
    3278             :   ///
    3279             :   /// The first and last endpoints of the selection will always be at the
    3280             :   /// beginning and end of a word respectively.
    3281           3 :   void selectWordsInRange(
    3282             :       {required Offset from,
    3283             :       Offset? to,
    3284             :       required SelectionChangedCause cause}) {
    3285           3 :     _layoutText(
    3286          12 :         minHeight: constraints.minHeight, maxHeight: constraints.maxHeight);
    3287             :     final firstPosition =
    3288          15 :         _textPainter.getPositionForOffset(globalToLocal(from - _paintOffset));
    3289           3 :     final firstWord = _selectWordAtOffset(firstPosition);
    3290             :     final lastWord = (to == null)
    3291             :         ? firstWord
    3292           2 :         : _selectWordAtOffset(_textPainter
    3293           4 :             .getPositionForOffset(globalToLocal(to - _paintOffset)));
    3294             : 
    3295           3 :     _setSelection(
    3296           3 :       TextSelection(
    3297           6 :         baseOffset: firstWord.base.offset,
    3298           6 :         extentOffset: lastWord.extent.offset,
    3299           3 :         affinity: firstWord.affinity,
    3300             :       ),
    3301             :       cause,
    3302             :     );
    3303             :   }
    3304             : 
    3305             :   /// Move the selection to the beginning or end of a word.
    3306           0 :   void selectWordEdge({required SelectionChangedCause cause}) {
    3307           0 :     _layoutText(
    3308           0 :         minHeight: constraints.minHeight, maxHeight: constraints.maxHeight);
    3309           0 :     assert(_lastTapDownPosition != null);
    3310           0 :     final position = _textPainter.getPositionForOffset(
    3311           0 :         globalToLocal(_lastTapDownPosition! - _paintOffset));
    3312           0 :     final word = _textPainter.getWordBoundary(position);
    3313           0 :     if (position.offset - word.start <= 1) {
    3314           0 :       _handleSelectionChange(
    3315           0 :         TextSelection.collapsed(
    3316           0 :             offset: word.start, affinity: TextAffinity.downstream),
    3317             :         cause,
    3318             :       );
    3319             :     } else {
    3320           0 :       _handleSelectionChange(
    3321           0 :         TextSelection.collapsed(
    3322           0 :             offset: word.end, affinity: TextAffinity.upstream),
    3323             :         cause,
    3324             :       );
    3325             :     }
    3326             :   }
    3327             : 
    3328           3 :   TextSelection _selectWordAtOffset(TextPosition position) {
    3329             :     assert(
    3330          12 :         _textLayoutLastMaxHeight == constraints.maxHeight &&
    3331          12 :             _textLayoutLastMinHeight == constraints.minHeight,
    3332           0 :         'Last height ($_textLayoutLastMinHeight, $_textLayoutLastMaxHeight) not the same as max height constraint (${constraints.minHeight}, ${constraints.maxHeight}).');
    3333           6 :     final word = _textPainter.getWordBoundary(position);
    3334             :     // When long-pressing past the end of the text, we want a collapsed cursor.
    3335           9 :     if (position.offset >= word.end) {
    3336           2 :       return TextSelection.fromPosition(position);
    3337             :     }
    3338             :     // If text is obscured, the entire sentence should be treated as one word.
    3339           3 :     if (obscureText) {
    3340           3 :       return TextSelection(baseOffset: 0, extentOffset: _plainText.length);
    3341             :       // On iOS, select the previous word if there is a previous word, or select
    3342             :       // to the end of the next word if there is a next word. Select nothing if
    3343             :       // there is neither a previous word nor a next word.
    3344             :       //
    3345             :       // If the platform is Android and the text is read only, try to select the
    3346             :       // previous word if there is one; otherwise, select the single whitespace at
    3347             :       // the position.
    3348          12 :     } else if (_isWhitespace(_plainText.codeUnitAt(position.offset)) &&
    3349           2 :         position.offset > 0) {
    3350           2 :       final previousWord = _getPreviousWord(word.start);
    3351           1 :       switch (defaultTargetPlatform) {
    3352           1 :         case TargetPlatform.iOS:
    3353             :           if (previousWord == null) {
    3354           2 :             final nextWord = _getNextWord(word.start);
    3355             :             if (nextWord == null) {
    3356           2 :               return TextSelection.collapsed(offset: position.offset);
    3357             :             }
    3358           1 :             return TextSelection(
    3359           1 :               baseOffset: position.offset,
    3360           1 :               extentOffset: nextWord.end,
    3361             :             );
    3362             :           }
    3363           0 :           return TextSelection(
    3364           0 :             baseOffset: previousWord.start,
    3365           0 :             extentOffset: position.offset,
    3366             :           );
    3367           1 :         case TargetPlatform.android:
    3368           1 :           if (readOnly) {
    3369             :             if (previousWord == null) {
    3370           1 :               return TextSelection(
    3371           1 :                 baseOffset: position.offset,
    3372           2 :                 extentOffset: position.offset + 1,
    3373             :               );
    3374             :             }
    3375           0 :             return TextSelection(
    3376           0 :               baseOffset: previousWord.start,
    3377           0 :               extentOffset: position.offset,
    3378             :             );
    3379             :           }
    3380             :           break;
    3381           0 :         case TargetPlatform.fuchsia:
    3382           0 :         case TargetPlatform.macOS:
    3383           0 :         case TargetPlatform.linux:
    3384           0 :         case TargetPlatform.windows:
    3385             :           break;
    3386             :       }
    3387             :     }
    3388             : 
    3389           9 :     return TextSelection(baseOffset: word.start, extentOffset: word.end);
    3390             :   }
    3391             : 
    3392           1 :   TextSelection _getLineAtOffset(TextPosition position) {
    3393             :     assert(
    3394           4 :         _textLayoutLastMaxHeight == constraints.maxHeight &&
    3395           4 :             _textLayoutLastMinHeight == constraints.minHeight,
    3396           0 :         'Last height ($_textLayoutLastMinHeight, $_textLayoutLastMaxHeight) not the same as max height constraint (${constraints.minHeight}, ${constraints.maxHeight}).');
    3397           2 :     final line = _textPainter.getLineBoundary(position);
    3398           3 :     if (position.offset >= line.end) {
    3399           1 :       return TextSelection.fromPosition(position);
    3400             :     }
    3401             :     // If text is obscured, the entire string should be treated as one line.
    3402           1 :     if (obscureText) {
    3403           0 :       return TextSelection(baseOffset: 0, extentOffset: _plainText.length);
    3404             :     }
    3405           3 :     return TextSelection(baseOffset: line.start, extentOffset: line.end);
    3406             :   }
    3407             : 
    3408           3 :   void _layoutText(
    3409             :       {double minHeight = 0.0, double maxHeight = double.infinity}) {
    3410           6 :     if (_textLayoutLastMaxHeight == maxHeight &&
    3411           6 :         _textLayoutLastMinHeight == minHeight) return;
    3412           9 :     final availableMaxHeight = math.max(0.0, maxHeight - _caretMargin);
    3413           3 :     final availableMinHeight = math.min(minHeight, availableMaxHeight);
    3414           3 :     final textMaxHeight = _isMultiline ? availableMaxHeight : double.infinity;
    3415           3 :     final textMinHeight = forceLine ? availableMaxHeight : availableMinHeight;
    3416           6 :     _textPainter.layout(
    3417             :       minHeight: textMinHeight,
    3418             :       maxHeight: textMaxHeight,
    3419             :     );
    3420           3 :     _textLayoutLastMinHeight = minHeight;
    3421           3 :     _textLayoutLastMaxHeight = maxHeight;
    3422             :   }
    3423             : 
    3424             :   late Rect _caretPrototype;
    3425             : 
    3426           3 :   void _computeCaretPrototype() {
    3427           3 :     switch (defaultTargetPlatform) {
    3428           3 :       case TargetPlatform.iOS:
    3429           3 :       case TargetPlatform.macOS:
    3430           3 :         _caretPrototype =
    3431          12 :             Rect.fromLTWH(0.0, 0.0, cursorWidth + 2, cursorHeight);
    3432             :         break;
    3433           3 :       case TargetPlatform.android:
    3434           2 :       case TargetPlatform.fuchsia:
    3435           2 :       case TargetPlatform.linux:
    3436           2 :       case TargetPlatform.windows:
    3437           6 :         _caretPrototype = Rect.fromLTWH(_kCaretWidthOffset, 0.0,
    3438          12 :             cursorWidth - 2.0 * _kCaretWidthOffset, cursorHeight);
    3439             :         break;
    3440             :     }
    3441             :   }
    3442             : 
    3443             :   // Computes the offset to apply to the given [sourceOffset] so it perfectly
    3444             :   // snaps to physical pixels.
    3445           3 :   Offset _snapToPhysicalPixel(Offset sourceOffset) {
    3446           3 :     final globalOffset = localToGlobal(sourceOffset);
    3447           6 :     final pixelMultiple = 1.0 / _devicePixelRatio;
    3448           3 :     return Offset(
    3449           6 :       globalOffset.dx.isFinite
    3450          15 :           ? (globalOffset.dx / pixelMultiple).round() * pixelMultiple -
    3451           3 :               globalOffset.dx
    3452             :           : 0,
    3453           6 :       globalOffset.dy.isFinite
    3454          15 :           ? (globalOffset.dy / pixelMultiple).round() * pixelMultiple -
    3455           3 :               globalOffset.dy
    3456             :           : 0,
    3457             :     );
    3458             :   }
    3459             : 
    3460           2 :   @override
    3461             :   Size computeDryLayout(BoxConstraints constraints) {
    3462           2 :     _layoutText(
    3463           4 :         minHeight: constraints.minHeight, maxHeight: constraints.maxHeight);
    3464           2 :     final height = forceLine
    3465           2 :         ? constraints.maxHeight
    3466           0 :         : constraints.constrainHeight(_textPainter.size.height + _caretMargin);
    3467           2 :     return Size(
    3468           6 :         constraints.constrainWidth(_preferredWidth(constraints.maxHeight)),
    3469             :         height);
    3470             :   }
    3471             : 
    3472           3 :   @override
    3473             :   void performLayout() {
    3474           3 :     final constraints = this.constraints;
    3475           3 :     _layoutText(
    3476           6 :         minHeight: constraints.minHeight, maxHeight: constraints.maxHeight);
    3477           3 :     _computeCaretPrototype();
    3478             :     // We grab _textPainter.size here because assigning to `size` on the next
    3479             :     // line will trigger us to validate our intrinsic sizes, which will change
    3480             :     // _textPainter's layout because the intrinsic size calculations are
    3481             :     // destructive, which would mean we would get different results if we later
    3482             :     // used properties on _textPainter in this method.
    3483             :     // Other _textPainter state like didExceedMaxLines will also be affected,
    3484             :     // though we currently don't use those here.
    3485             :     // See also MongolRenderParagraph which has a similar issue.
    3486           6 :     final textPainterSize = _textPainter.size;
    3487           3 :     final height = forceLine
    3488           3 :         ? constraints.maxHeight
    3489           0 :         : constraints.constrainHeight(_textPainter.size.height + _caretMargin);
    3490           6 :     size = Size(
    3491           9 :         constraints.constrainWidth(_preferredWidth(constraints.maxHeight)),
    3492             :         height);
    3493             :     final contentSize =
    3494          15 :         Size(textPainterSize.width, textPainterSize.height + _caretMargin);
    3495             : 
    3496           3 :     final painterConstraints = BoxConstraints.tight(contentSize);
    3497             : 
    3498           6 :     _foregroundRenderObject?.layout(painterConstraints);
    3499           6 :     _backgroundRenderObject?.layout(painterConstraints);
    3500             : 
    3501           6 :     _maxScrollExtent = _getMaxScrollExtent(contentSize);
    3502           9 :     offset.applyViewportDimension(_viewportExtent);
    3503           9 :     offset.applyContentDimensions(0.0, _maxScrollExtent);
    3504             :   }
    3505             : 
    3506           3 :   void _paintContents(PaintingContext context, Offset offset) {
    3507             :     assert(
    3508          12 :         _textLayoutLastMaxHeight == constraints.maxHeight &&
    3509          12 :             _textLayoutLastMinHeight == constraints.minHeight,
    3510           0 :         'Last height ($_textLayoutLastMinHeight, $_textLayoutLastMaxHeight) not the same as max height constraint (${constraints.minHeight}, ${constraints.maxHeight}).');
    3511           6 :     final effectiveOffset = offset + _paintOffset;
    3512             : 
    3513           3 :     if (selection != null) {
    3514           3 :       _updateSelectionExtentsVisibility(effectiveOffset);
    3515             :     }
    3516             : 
    3517           3 :     final RenderBox? foregroundChild = _foregroundRenderObject;
    3518           3 :     final RenderBox? backgroundChild = _backgroundRenderObject;
    3519             : 
    3520             :     // The painters paint in the viewport's coordinate space, since the
    3521             :     // textPainter's coordinate space is not known to high level widgets.
    3522           3 :     if (backgroundChild != null) context.paintChild(backgroundChild, offset);
    3523             : 
    3524           9 :     _textPainter.paint(context.canvas, effectiveOffset);
    3525             : 
    3526           3 :     if (foregroundChild != null) context.paintChild(foregroundChild, offset);
    3527             :   }
    3528             : 
    3529           3 :   void _paintHandleLayers(
    3530             :       PaintingContext context, List<TextSelectionPoint> endpoints) {
    3531           6 :     var startPoint = endpoints[0].point;
    3532           3 :     startPoint = Offset(
    3533          12 :       startPoint.dx.clamp(0.0, size.width),
    3534          12 :       startPoint.dy.clamp(0.0, size.height),
    3535             :     );
    3536           3 :     context.pushLayer(
    3537           6 :       LeaderLayer(link: startHandleLayerLink, offset: startPoint),
    3538             :       super.paint,
    3539             :       Offset.zero,
    3540             :     );
    3541           6 :     if (endpoints.length == 2) {
    3542           6 :       var endPoint = endpoints[1].point;
    3543           3 :       endPoint = Offset(
    3544          12 :         endPoint.dx.clamp(0.0, size.width),
    3545          12 :         endPoint.dy.clamp(0.0, size.height),
    3546             :       );
    3547           3 :       context.pushLayer(
    3548           6 :         LeaderLayer(link: endHandleLayerLink, offset: endPoint),
    3549             :         super.paint,
    3550             :         Offset.zero,
    3551             :       );
    3552             :     }
    3553             :   }
    3554             : 
    3555           3 :   @override
    3556             :   void paint(PaintingContext context, Offset offset) {
    3557           3 :     _layoutText(
    3558          12 :         minHeight: constraints.minHeight, maxHeight: constraints.maxHeight);
    3559           9 :     if (_hasVisualOverflow && clipBehavior != Clip.none) {
    3560           6 :       _clipRectLayer = context.pushClipRect(
    3561           3 :         needsCompositing,
    3562             :         offset,
    3563           6 :         Offset.zero & size,
    3564           3 :         _paintContents,
    3565           3 :         clipBehavior: clipBehavior,
    3566           3 :         oldLayer: _clipRectLayer,
    3567             :       );
    3568             :     } else {
    3569           3 :       _clipRectLayer = null;
    3570           3 :       _paintContents(context, offset);
    3571             :     }
    3572           9 :     _paintHandleLayers(context, getEndpointsForSelection(selection!));
    3573             :   }
    3574             : 
    3575             :   ClipRectLayer? _clipRectLayer;
    3576             : 
    3577           0 :   @override
    3578             :   Rect? describeApproximatePaintClip(RenderObject child) =>
    3579           0 :       _hasVisualOverflow ? Offset.zero & size : null;
    3580             : 
    3581           1 :   @override
    3582             :   void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    3583           1 :     super.debugFillProperties(properties);
    3584           3 :     properties.add(ColorProperty('cursorColor', cursorColor));
    3585           1 :     properties.add(
    3586           2 :         DiagnosticsProperty<ValueNotifier<bool>>('showCursor', showCursor));
    3587           3 :     properties.add(IntProperty('maxLines', maxLines));
    3588           3 :     properties.add(IntProperty('minLines', minLines));
    3589           1 :     properties.add(
    3590           2 :         DiagnosticsProperty<bool>('expands', expands, defaultValue: false));
    3591           3 :     properties.add(ColorProperty('selectionColor', selectionColor));
    3592           3 :     properties.add(DoubleProperty('textScaleFactor', textScaleFactor));
    3593           3 :     properties.add(DiagnosticsProperty<TextSelection>('selection', selection));
    3594           3 :     properties.add(DiagnosticsProperty<ViewportOffset>('offset', offset));
    3595             :   }
    3596             : 
    3597           1 :   @override
    3598             :   List<DiagnosticsNode> debugDescribeChildren() {
    3599           1 :     return <DiagnosticsNode>[
    3600           1 :       if (text != null)
    3601           2 :         text!.toDiagnosticsNode(
    3602             :           name: 'text',
    3603             :           style: DiagnosticsTreeStyle.transition,
    3604             :         ),
    3605             :     ];
    3606             :   }
    3607             : }
    3608             : 
    3609             : class _MongolRenderEditableCustomPaint extends RenderBox {
    3610           3 :   _MongolRenderEditableCustomPaint({
    3611             :     MongolRenderEditablePainter? painter,
    3612             :   })  : _painter = painter,
    3613           3 :         super();
    3614             : 
    3615           3 :   @override
    3616           3 :   MongolRenderEditable? get parent => super.parent as MongolRenderEditable?;
    3617             : 
    3618           3 :   @override
    3619             :   bool get isRepaintBoundary => true;
    3620             : 
    3621           3 :   @override
    3622             :   bool get sizedByParent => true;
    3623             : 
    3624           6 :   MongolRenderEditablePainter? get painter => _painter;
    3625             :   MongolRenderEditablePainter? _painter;
    3626           1 :   set painter(MongolRenderEditablePainter? newValue) {
    3627           2 :     if (newValue == painter) return;
    3628             : 
    3629           1 :     final oldPainter = painter;
    3630           1 :     _painter = newValue;
    3631             : 
    3632           2 :     if (newValue?.shouldRepaint(oldPainter) ?? true) markNeedsPaint();
    3633             : 
    3634           1 :     if (attached) {
    3635           2 :       oldPainter?.removeListener(markNeedsPaint);
    3636           2 :       newValue?.addListener(markNeedsPaint);
    3637             :     }
    3638             :   }
    3639             : 
    3640           3 :   @override
    3641             :   void paint(PaintingContext context, Offset offset) {
    3642           3 :     final parent = this.parent;
    3643           0 :     assert(parent != null);
    3644           3 :     final painter = this.painter;
    3645             :     if (painter != null && parent != null) {
    3646           9 :       painter.paint(context.canvas, size, parent);
    3647             :     }
    3648             :   }
    3649             : 
    3650           3 :   @override
    3651             :   void attach(PipelineOwner owner) {
    3652           3 :     super.attach(owner);
    3653           9 :     _painter?.addListener(markNeedsPaint);
    3654             :   }
    3655             : 
    3656           3 :   @override
    3657             :   void detach() {
    3658           9 :     _painter?.removeListener(markNeedsPaint);
    3659           3 :     super.detach();
    3660             :   }
    3661             : 
    3662           3 :   @override
    3663           3 :   Size computeDryLayout(BoxConstraints constraints) => constraints.biggest;
    3664             : }
    3665             : 
    3666             : /// An interface that paints within a [MongolRenderEditable]'s bounds, above or
    3667             : /// beneath its text content.
    3668             : ///
    3669             : /// This painter is typically used for painting auxiliary content that depends
    3670             : /// on text layout metrics (for instance, for painting carets and text highlight
    3671             : /// blocks). It can paint independently from its [MongolRenderEditable],
    3672             : /// allowing it to repaint without triggering a repaint on the entire
    3673             : /// [MongolRenderEditable] stack when only auxiliary content changes (e.g. a
    3674             : /// blinking cursor) are present. It will be scheduled to repaint when:
    3675             : ///
    3676             : ///  * It's assigned to a new [MongolRenderEditable] and the [shouldRepaint]
    3677             : ///    method returns true.
    3678             : ///  * Any of the [MongolRenderEditable]s it is attached to repaints.
    3679             : ///  * The [notifyListeners] method is called, which typically happens when the
    3680             : ///    painter's attributes change.
    3681             : ///
    3682             : /// See also:
    3683             : ///
    3684             : ///  * [MongolRenderEditable.foregroundPainter], which takes a
    3685             : ///    [MongolRenderEditablePainter] and sets it as the foreground painter of
    3686             : ///    the [MongolRenderEditable].
    3687             : ///  * [MongolRenderEditable.painter], which takes a [MongolRenderEditablePainter]
    3688             : ///    and sets it as the background painter of the [MongolRenderEditable].
    3689             : ///  * [CustomPainter] a similar class which paints within a [RenderCustomPaint].
    3690             : abstract class MongolRenderEditablePainter extends ChangeNotifier {
    3691             :   /// Determines whether repaint is needed when a new
    3692             :   /// [MongolRenderEditablePainter] is provided to a [MongolRenderEditable].
    3693             :   ///
    3694             :   /// If the new instance represents different information than the old
    3695             :   /// instance, then the method should return true, otherwise it should return
    3696             :   /// false. When [oldDelegate] is null, this method should always return true
    3697             :   /// unless the new painter initially does not paint anything.
    3698             :   ///
    3699             :   /// If the method returns false, then the [paint] call might be optimized
    3700             :   /// away. However, the [paint] method will get called whenever the
    3701             :   /// [MongolRenderEditable]s it attaches to repaint, even if [shouldRepaint]
    3702             :   /// returns false.
    3703             :   bool shouldRepaint(MongolRenderEditablePainter? oldDelegate);
    3704             : 
    3705             :   /// Paints within the bounds of a [MongolRenderEditable].
    3706             :   ///
    3707             :   /// The given [Canvas] has the same coordinate space as the
    3708             :   /// [MongolRenderEditable], which may be different from the coordinate space
    3709             :   /// the [MongolRenderEditable]'s [MongolTextPainter] uses, when the text moves
    3710             :   /// inside the [MongolRenderEditable].
    3711             :   ///
    3712             :   /// Paint operations performed outside of the region defined by the [canvas]'s
    3713             :   /// origin and the [size] parameter may get clipped, when
    3714             :   /// [MongolRenderEditable]'s [MongolRenderEditable.clipBehavior] is not
    3715             :   /// [Clip.none].
    3716             :   void paint(Canvas canvas, Size size, MongolRenderEditable renderEditable);
    3717             : }
    3718             : 
    3719             : class _TextHighlightPainter extends MongolRenderEditablePainter {
    3720           3 :   _TextHighlightPainter({TextRange? highlightedRange, Color? highlightColor})
    3721             :       : _highlightedRange = highlightedRange,
    3722             :         _highlightColor = highlightColor;
    3723             : 
    3724             :   final Paint highlightPaint = Paint();
    3725             : 
    3726           6 :   Color? get highlightColor => _highlightColor;
    3727             :   Color? _highlightColor;
    3728           3 :   set highlightColor(Color? newValue) {
    3729           6 :     if (newValue == _highlightColor) return;
    3730           2 :     _highlightColor = newValue;
    3731           2 :     notifyListeners();
    3732             :   }
    3733             : 
    3734           6 :   TextRange? get highlightedRange => _highlightedRange;
    3735             :   TextRange? _highlightedRange;
    3736           3 :   set highlightedRange(TextRange? newValue) {
    3737           6 :     if (newValue == _highlightedRange) return;
    3738           3 :     _highlightedRange = newValue;
    3739           3 :     notifyListeners();
    3740             :   }
    3741             : 
    3742           3 :   @override
    3743             :   void paint(Canvas canvas, Size size, MongolRenderEditable renderEditable) {
    3744           3 :     final range = highlightedRange;
    3745           3 :     final color = highlightColor;
    3746           2 :     if (range == null || color == null || range.isCollapsed) {
    3747             :       return;
    3748             :     }
    3749             : 
    3750           4 :     highlightPaint.color = color;
    3751           4 :     final boxes = renderEditable._textPainter.getBoxesForSelection(
    3752           6 :       TextSelection(baseOffset: range.start, extentOffset: range.end),
    3753             :     );
    3754             : 
    3755           4 :     for (final box in boxes) {
    3756           8 :       canvas.drawRect(box.shift(renderEditable._paintOffset), highlightPaint);
    3757             :     }
    3758             :   }
    3759             : 
    3760           0 :   @override
    3761             :   bool shouldRepaint(MongolRenderEditablePainter? oldDelegate) {
    3762             :     if (identical(oldDelegate, this)) {
    3763             :       return false;
    3764             :     }
    3765             :     if (oldDelegate == null) {
    3766           0 :       return highlightColor != null && highlightedRange != null;
    3767             :     }
    3768           0 :     return oldDelegate is! _TextHighlightPainter ||
    3769           0 :         oldDelegate.highlightColor != highlightColor ||
    3770           0 :         oldDelegate.highlightedRange != highlightedRange;
    3771             :   }
    3772             : }
    3773             : 
    3774             : class _CaretPainter extends MongolRenderEditablePainter {
    3775           3 :   _CaretPainter(this.caretPaintCallback);
    3776             : 
    3777           6 :   bool get shouldPaint => _shouldPaint;
    3778             :   bool _shouldPaint = true;
    3779           3 :   set shouldPaint(bool value) {
    3780           6 :     if (shouldPaint == value) return;
    3781           3 :     _shouldPaint = value;
    3782           3 :     notifyListeners();
    3783             :   }
    3784             : 
    3785             :   CaretChangedHandler caretPaintCallback;
    3786             : 
    3787             :   final Paint caretPaint = Paint();
    3788             :   late final Paint floatingCursorPaint = Paint();
    3789             : 
    3790           6 :   Color? get caretColor => _caretColor;
    3791             :   Color? _caretColor;
    3792           3 :   set caretColor(Color? value) {
    3793          12 :     if (caretColor?.value == value?.value) return;
    3794             : 
    3795           3 :     _caretColor = value;
    3796           3 :     notifyListeners();
    3797             :   }
    3798             : 
    3799           6 :   Radius? get cursorRadius => _cursorRadius;
    3800             :   Radius? _cursorRadius;
    3801           3 :   set cursorRadius(Radius? value) {
    3802           6 :     if (_cursorRadius == value) return;
    3803           2 :     _cursorRadius = value;
    3804           2 :     notifyListeners();
    3805             :   }
    3806             : 
    3807           6 :   Offset get cursorOffset => _cursorOffset;
    3808             :   Offset _cursorOffset = Offset.zero;
    3809           3 :   set cursorOffset(Offset value) {
    3810           6 :     if (_cursorOffset == value) return;
    3811           1 :     _cursorOffset = value;
    3812           1 :     notifyListeners();
    3813             :   }
    3814             : 
    3815           3 :   void paintRegularCursor(Canvas canvas, MongolRenderEditable renderEditable,
    3816             :       Color caretColor, TextPosition textPosition) {
    3817           3 :     final caretPrototype = renderEditable._caretPrototype;
    3818           3 :     final caretOffset = renderEditable._textPainter
    3819           3 :         .getOffsetForCaret(textPosition, caretPrototype);
    3820           9 :     var caretRect = caretPrototype.shift(caretOffset + cursorOffset);
    3821             : 
    3822           3 :     final caretWidth = renderEditable._textPainter
    3823           3 :         .getFullWidthForCaret(textPosition, caretPrototype);
    3824             :     if (caretWidth != null) {
    3825           3 :       caretRect = Rect.fromLTWH(
    3826           6 :         caretRect.left - _kCaretWidthOffset,
    3827           3 :         caretRect.top,
    3828             :         caretWidth,
    3829           3 :         caretRect.height,
    3830             :       );
    3831             :     }
    3832             : 
    3833           6 :     caretRect = caretRect.shift(renderEditable._paintOffset);
    3834             :     final integralRect =
    3835           9 :         caretRect.shift(renderEditable._snapToPhysicalPixel(caretRect.topLeft));
    3836             : 
    3837           3 :     if (shouldPaint) {
    3838           3 :       final radius = cursorRadius;
    3839           6 :       caretPaint.color = caretColor;
    3840             :       if (radius == null) {
    3841           6 :         canvas.drawRect(integralRect, caretPaint);
    3842             :       } else {
    3843           2 :         final caretRRect = RRect.fromRectAndRadius(integralRect, radius);
    3844           4 :         canvas.drawRRect(caretRRect, caretPaint);
    3845             :       }
    3846             :     }
    3847           6 :     caretPaintCallback(integralRect);
    3848             :   }
    3849             : 
    3850           3 :   @override
    3851             :   void paint(Canvas canvas, Size size, MongolRenderEditable renderEditable) {
    3852             :     // Compute the caret location even when `shouldPaint` is false.
    3853             : 
    3854           3 :     final selection = renderEditable.selection;
    3855             : 
    3856           3 :     if (selection == null || !selection.isCollapsed) {
    3857             :       return;
    3858             :     }
    3859             : 
    3860           3 :     final caretColor = this.caretColor;
    3861           3 :     final caretTextPosition = selection.extent;
    3862             : 
    3863             :     if (caretColor != null) {
    3864           3 :       paintRegularCursor(canvas, renderEditable, caretColor, caretTextPosition);
    3865             :     }
    3866             :   }
    3867             : 
    3868           0 :   @override
    3869             :   bool shouldRepaint(MongolRenderEditablePainter? oldDelegate) {
    3870             :     if (identical(this, oldDelegate)) return false;
    3871             : 
    3872           0 :     if (oldDelegate == null) return shouldPaint;
    3873           0 :     return oldDelegate is! _CaretPainter ||
    3874           0 :         oldDelegate.shouldPaint != shouldPaint ||
    3875           0 :         oldDelegate.caretColor != caretColor ||
    3876           0 :         oldDelegate.cursorRadius != cursorRadius ||
    3877           0 :         oldDelegate.cursorOffset != cursorOffset;
    3878             :   }
    3879             : }
    3880             : 
    3881             : class _CompositeRenderEditablePainter extends MongolRenderEditablePainter {
    3882           3 :   _CompositeRenderEditablePainter({required this.painters});
    3883             : 
    3884             :   final List<MongolRenderEditablePainter> painters;
    3885             : 
    3886           3 :   @override
    3887             :   void addListener(VoidCallback listener) {
    3888           6 :     for (final painter in painters) {
    3889           3 :       painter.addListener(listener);
    3890             :     }
    3891             :   }
    3892             : 
    3893           3 :   @override
    3894             :   void removeListener(VoidCallback listener) {
    3895           6 :     for (final painter in painters) {
    3896           3 :       painter.removeListener(listener);
    3897             :     }
    3898             :   }
    3899             : 
    3900           3 :   @override
    3901             :   void paint(Canvas canvas, Size size, MongolRenderEditable renderEditable) {
    3902           6 :     for (final painter in painters) {
    3903           3 :       painter.paint(canvas, size, renderEditable);
    3904             :     }
    3905             :   }
    3906             : 
    3907           1 :   @override
    3908             :   bool shouldRepaint(MongolRenderEditablePainter? oldDelegate) {
    3909             :     if (identical(oldDelegate, this)) return false;
    3910           1 :     if (oldDelegate is! _CompositeRenderEditablePainter ||
    3911           5 :         oldDelegate.painters.length != painters.length) return true;
    3912             : 
    3913           2 :     final oldPainters = oldDelegate.painters.iterator;
    3914           2 :     final newPainters = painters.iterator;
    3915           2 :     while (oldPainters.moveNext() && newPainters.moveNext()) {
    3916           3 :       if (newPainters.current.shouldRepaint(oldPainters.current)) return true;
    3917             :     }
    3918             : 
    3919             :     return false;
    3920             :   }
    3921             : }

Generated by: LCOV version 1.15