LCOV - code coverage report
Current view: top level - editing - mongol_editable_text.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 609 668 91.2 %
Date: 2021-07-30 09:13:58 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:async' show Timer;
       8             : import 'dart:math' as math;
       9             : import 'dart:ui' as ui hide TextStyle;
      10             : 
      11             : import 'package:flutter/foundation.dart';
      12             : import 'package:flutter/gestures.dart' show DragStartBehavior;
      13             : import 'package:flutter/material.dart' show kMinInteractiveDimension;
      14             : import 'package:flutter/rendering.dart' show RevealedOffset, ViewportOffset, CaretChangedHandler;
      15             : import 'package:flutter/scheduler.dart' show SchedulerBinding;
      16             : import 'package:flutter/services.dart';
      17             : import 'package:flutter/widgets.dart' hide EditableText, EditableTextState;
      18             : 
      19             : import 'package:mongol/src/base/mongol_text_align.dart';
      20             : import 'package:mongol/src/editing/mongol_render_editable.dart';
      21             : import 'package:mongol/src/editing/text_selection/mongol_text_selection.dart';
      22             : 
      23             : import 'mongol_text_editing_action.dart';
      24             : 
      25             : // ignore_for_file: todo
      26             : 
      27             : // The time it takes for the cursor to fade from fully opaque to fully
      28             : // transparent and vice versa. A full cursor blink, from transparent to opaque
      29             : // to transparent, is twice this duration.
      30             : const Duration _kCursorBlinkHalfPeriod = Duration(milliseconds: 500);
      31             : 
      32             : // The time the cursor is static in opacity before animating to become
      33             : // transparent.
      34             : const Duration _kCursorBlinkWaitForStart = Duration(milliseconds: 150);
      35             : 
      36             : // Number of cursor ticks during which the most recently entered character
      37             : // is shown in an obscured text field.
      38             : const int _kObscureShowLatestCharCursorTicks = 3;
      39             : 
      40             : /// A basic text input field.
      41             : ///
      42             : /// This widget interacts with the [TextInput] service to let the user edit the
      43             : /// text it contains. It also provides scrolling, selection, and cursor
      44             : /// movement. This widget does not provide any focus management (e.g.,
      45             : /// tap-to-focus).
      46             : ///
      47             : /// ## Handling User Input
      48             : ///
      49             : /// Currently the user may change the text this widget contains via keyboard or
      50             : /// the text selection menu. When the user inserted or deleted text, you will be
      51             : /// notified of the change and get a chance to modify the new text value:
      52             : ///
      53             : /// * The [inputFormatters] will be first applied to the user input.
      54             : ///
      55             : /// * The [controller]'s [TextEditingController.value] will be updated with the
      56             : ///   formatted result, and the [controller]'s listeners will be notified.
      57             : ///
      58             : /// * The [onChanged] callback, if specified, will be called last.
      59             : ///
      60             : /// ## Input Actions
      61             : ///
      62             : /// A [TextInputAction] can be provided to customize the appearance of the
      63             : /// action button on the soft keyboard for Android and iOS. The default action
      64             : /// is [TextInputAction.done].
      65             : ///
      66             : /// Many [TextInputAction]s are common between Android and iOS. However, if a
      67             : /// [textInputAction] is provided that is not supported by the current
      68             : /// platform in debug mode, an error will be thrown when the corresponding
      69             : /// MongolEditableText receives focus. For example, providing iOS's "emergencyCall"
      70             : /// action when running on an Android device will result in an error when in
      71             : /// debug mode. In release mode, incompatible [TextInputAction]s are replaced
      72             : /// either with "unspecified" on Android, or "default" on iOS. Appropriate
      73             : /// [textInputAction]s can be chosen by checking the current platform and then
      74             : /// selecting the appropriate action.
      75             : ///
      76             : /// ## Lifecycle
      77             : ///
      78             : /// Upon completion of editing, like pressing the "done" button on the keyboard,
      79             : /// two actions take place:
      80             : ///
      81             : ///   1st: Editing is finalized. The default behavior of this step includes
      82             : ///   an invocation of [onChanged]. That default behavior can be overridden.
      83             : ///   See [onEditingComplete] for details.
      84             : ///
      85             : ///   2nd: [onSubmitted] is invoked with the user's input value.
      86             : ///
      87             : /// [onSubmitted] can be used to manually move focus to another input widget
      88             : /// when a user finishes with the currently focused input widget.
      89             : ///
      90             : /// Rather than using this widget directly, consider using [MongolTextField], which
      91             : /// is a full-featured, material-design text input field with placeholder text,
      92             : /// labels, and [Form] integration.
      93             : ///
      94             : /// ## Gesture Events Handling
      95             : ///
      96             : /// This widget provides rudimentary, platform-agnostic gesture handling for
      97             : /// user actions such as tapping, long-pressing and scrolling when
      98             : /// [rendererIgnoresPointer] is false (false by default). For custom selection
      99             : /// behavior, call methods such as [MongolRenderEditable.selectPosition],
     100             : /// [MongolRenderEditable.selectWord], etc. programmatically.
     101             : ///
     102             : /// See also:
     103             : ///
     104             : ///  * [MongolTextField], which is a full-featured, material-design text input
     105             : ///    field with placeholder text, labels, and [Form] integration.
     106             : class MongolEditableText extends StatefulWidget {
     107             :   /// Creates a basic text input control.
     108             :   ///
     109             :   /// The [maxLines] property can be set to null to remove the restriction on
     110             :   /// the number of lines. By default, it is one, meaning this is a single-line
     111             :   /// text field. [maxLines] must be null or greater than zero.
     112             :   ///
     113             :   /// If [keyboardType] is not set or is null, its value will be inferred from
     114             :   /// [autofillHints], if [autofillHints] is not empty. Otherwise it defaults to
     115             :   /// [TextInputType.text] if [maxLines] is exactly one, and
     116             :   /// [TextInputType.multiline] if [maxLines] is null or greater than one.
     117             :   ///
     118             :   /// The text cursor is not shown if [showCursor] is false or if [showCursor]
     119             :   /// is null (the default) and [readOnly] is true.
     120           2 :   MongolEditableText({
     121             :     Key? key,
     122             :     required this.controller,
     123             :     required this.focusNode,
     124             :     this.readOnly = false,
     125             :     this.obscuringCharacter = '•',
     126             :     this.obscureText = false,
     127             :     this.autocorrect = true,
     128             :     this.enableSuggestions = true,
     129             :     required this.style,
     130             :     required this.cursorColor,
     131             :     this.textAlign = MongolTextAlign.top,
     132             :     this.textScaleFactor,
     133             :     this.maxLines = 1,
     134             :     this.minLines,
     135             :     this.expands = false,
     136             :     this.forceLine = true,
     137             :     this.autofocus = false,
     138             :     bool? showCursor,
     139             :     this.showSelectionHandles = false,
     140             :     this.selectionColor,
     141             :     this.selectionControls,
     142             :     TextInputType? keyboardType,
     143             :     this.textInputAction,
     144             :     this.onChanged,
     145             :     this.onEditingComplete,
     146             :     this.onSubmitted,
     147             :     this.onAppPrivateCommand,
     148             :     this.onSelectionChanged,
     149             :     this.onSelectionHandleTapped,
     150             :     List<TextInputFormatter>? inputFormatters,
     151             :     this.mouseCursor,
     152             :     this.rendererIgnoresPointer = false,
     153             :     this.cursorWidth,
     154             :     this.cursorHeight = 2.0,
     155             :     this.cursorRadius,
     156             :     this.cursorOpacityAnimates = false,
     157             :     this.cursorOffset,
     158             :     this.scrollPadding = const EdgeInsets.all(20.0),
     159             :     this.keyboardAppearance = Brightness.light,
     160             :     this.dragStartBehavior = DragStartBehavior.start,
     161             :     this.enableInteractiveSelection = true,
     162             :     this.scrollController,
     163             :     this.scrollPhysics,
     164             :     this.toolbarOptions = const ToolbarOptions(
     165             :       copy: true,
     166             :       cut: true,
     167             :       paste: true,
     168             :       selectAll: true,
     169             :     ),
     170             :     this.autofillHints,
     171             :     this.clipBehavior = Clip.hardEdge,
     172             :     this.restorationId,
     173             :     this.scrollBehavior,
     174           4 :   })  : assert(obscuringCharacter.length == 1),
     175           2 :         assert(maxLines == null || maxLines > 0),
     176           1 :         assert(minLines == null || minLines > 0),
     177             :         assert(
     178           1 :           (maxLines == null) || (minLines == null) || (maxLines >= minLines),
     179             :           "minLines can't be greater than maxLines",
     180             :         ),
     181             :         assert(
     182           0 :           !expands || (maxLines == null && minLines == null),
     183             :           'minLines and maxLines must be null when expands is true.',
     184             :         ),
     185           3 :         assert(!obscureText || maxLines == 1,
     186             :             'Obscured fields cannot be multiline.'),
     187             :         assert(
     188           0 :           !readOnly || autofillHints == null,
     189             :           "Read-only fields can't have autofill hints.",
     190             :         ),
     191             :         keyboardType = keyboardType ??
     192           1 :             _inferKeyboardType(
     193             :                 autofillHints: autofillHints, maxLines: maxLines),
     194           2 :         inputFormatters = maxLines == 1
     195           2 :             ? <TextInputFormatter>[
     196           2 :                 FilteringTextInputFormatter.singleLineFormatter,
     197           2 :                 ...inputFormatters ??
     198             :                     const Iterable<TextInputFormatter>.empty(),
     199             :               ]
     200             :             : inputFormatters,
     201             :         showCursor = showCursor ?? !readOnly,
     202           2 :         super(key: key);
     203             : 
     204             :   /// Controls the text being edited.
     205             :   final TextEditingController controller;
     206             : 
     207             :   /// Controls whether this widget has keyboard focus.
     208             :   final FocusNode focusNode;
     209             : 
     210             :   /// Character used for obscuring text if [obscureText] is true.
     211             :   ///
     212             :   /// Must be only a single character.
     213             :   ///
     214             :   /// Defaults to the character U+2022 BULLET (•).
     215             :   final String obscuringCharacter;
     216             : 
     217             :   /// Whether to hide the text being edited (e.g., for passwords).
     218             :   ///
     219             :   /// When this is set to true, all the characters in the text field are
     220             :   /// replaced by [obscuringCharacter].
     221             :   ///
     222             :   /// Defaults to false.
     223             :   final bool obscureText;
     224             : 
     225             :   /// Whether the text can be changed.
     226             :   ///
     227             :   /// When this is set to true, the text cannot be modified
     228             :   /// by any shortcut or keyboard operation. The text is still selectable.
     229             :   ///
     230             :   /// Defaults to false.
     231             :   final bool readOnly;
     232             : 
     233             :   /// Whether the text will take the full height regardless of the text height.
     234             :   ///
     235             :   /// When this is set to false, the height will be based on text height.
     236             :   ///
     237             :   /// Defaults to true.
     238             :   ///
     239             :   /// See also:
     240             :   ///
     241             :   ///  * [textWidthBasis], which controls the calculation of text width.
     242             :   final bool forceLine;
     243             : 
     244             :   /// Configuration of toolbar options.
     245             :   ///
     246             :   /// By default, all options are enabled. If [readOnly] is true,
     247             :   /// paste and cut will be disabled regardless.
     248             :   final ToolbarOptions toolbarOptions;
     249             : 
     250             :   /// Whether to show selection handles.
     251             :   ///
     252             :   /// When a selection is active, there will be two handles at each side of
     253             :   /// boundary, or one handle if the selection is collapsed. The handles can be
     254             :   /// dragged to adjust the selection.
     255             :   ///
     256             :   /// See also:
     257             :   ///
     258             :   ///  * [showCursor], which controls the visibility of the cursor.
     259             :   final bool showSelectionHandles;
     260             : 
     261             :   /// Whether to show cursor.
     262             :   ///
     263             :   /// The cursor refers to the blinking caret when the [MongolEditableText] is
     264             :   /// focused.
     265             :   ///
     266             :   /// See also:
     267             :   ///
     268             :   ///  * [showSelectionHandles], which controls the visibility of the selection
     269             :   ///    handles.
     270             :   final bool showCursor;
     271             : 
     272             :   /// Whether to enable autocorrection.
     273             :   ///
     274             :   /// Defaults to true. Cannot be null.
     275             :   final bool autocorrect;
     276             : 
     277             :   /// Whether to show input suggestions as the user types.
     278             :   ///
     279             :   /// This flag only affects Android. On iOS, suggestions are tied directly to
     280             :   /// [autocorrect], so that suggestions are only shown when [autocorrect] is
     281             :   /// true. On Android autocorrection and suggestion are controlled separately.
     282             :   ///
     283             :   /// Defaults to true.
     284             :   ///
     285             :   /// See also:
     286             :   ///
     287             :   ///  * <https://developer.android.com/reference/android/text/InputType.html#TYPE_TEXT_FLAG_NO_SUGGESTIONS>
     288             :   final bool enableSuggestions;
     289             : 
     290             :   /// The text style to use for the editable text.
     291             :   final TextStyle style;
     292             : 
     293             :   /// How the text should be aligned vertically.
     294             :   ///
     295             :   /// Defaults to [MongolTextAlign.top].
     296             :   final MongolTextAlign textAlign;
     297             : 
     298             :   /// The number of font pixels for each logical pixel.
     299             :   ///
     300             :   /// For example, if the text scale factor is 1.5, text will be 50% larger than
     301             :   /// the specified font size.
     302             :   ///
     303             :   /// Defaults to the [MediaQueryData.textScaleFactor] obtained from the ambient
     304             :   /// [MediaQuery], or 1.0 if there is no [MediaQuery] in scope.
     305             :   final double? textScaleFactor;
     306             : 
     307             :   /// The color to use when painting the cursor.
     308             :   final Color cursorColor;
     309             : 
     310             :   /// The maximum number of lines for the text to span, wrapping if necessary.
     311             :   ///
     312             :   /// If this is 1 (the default), the text will not wrap, but will scroll
     313             :   /// vertically instead.
     314             :   ///
     315             :   /// If this is null, there is no limit to the number of lines, and the text
     316             :   /// container will start with enough horizontal space for one line and
     317             :   /// automatically grow to accommodate additional lines as they are entered.
     318             :   ///
     319             :   /// If this is not null, the value must be greater than zero, and it will lock
     320             :   /// the input to the given number of lines and take up enough vertical space
     321             :   /// to accommodate that number of lines. Setting [minLines] as well allows the
     322             :   /// input to grow between the indicated range.
     323             :   ///
     324             :   /// The full set of behaviors possible with [minLines] and [maxLines] are as
     325             :   /// follows. These examples apply equally to `MongolTextField`,
     326             :   /// `MongolTextFormField`, and `MongolEditableText`.
     327             :   ///
     328             :   /// Input that occupies a single line and scrolls vertically as needed.
     329             :   /// ```dart
     330             :   /// MongolTextField()
     331             :   /// ```
     332             :   ///
     333             :   /// Input whose width grows from one line up to as many lines as needed for
     334             :   /// the text that was entered. If a width limit is imposed by its parent, it
     335             :   /// will scroll horizontally when its width reaches that limit.
     336             :   /// ```dart
     337             :   /// MongolTextField(maxLines: null)
     338             :   /// ```
     339             :   ///
     340             :   /// The input's width is large enough for the given number of lines. If
     341             :   /// additional lines are entered the input scrolls horizontally.
     342             :   /// ```dart
     343             :   /// MongolTextField(maxLines: 2)
     344             :   /// ```
     345             :   ///
     346             :   /// Input whose width grows with content between a min and max. An infinite
     347             :   /// max is possible with `maxLines: null`.
     348             :   /// ```dart
     349             :   /// MongolTextField(minLines: 2, maxLines: 4)
     350             :   /// ```
     351             :   final int? maxLines;
     352             : 
     353             :   /// The minimum number of lines to occupy when the content spans fewer lines.
     354             :   ///
     355             :   /// If this is null (default), text container starts with enough horizontal space
     356             :   /// for one line and grows to accommodate additional lines as they are entered.
     357             :   ///
     358             :   /// This can be used in combination with [maxLines] for a varying set of behaviors.
     359             :   ///
     360             :   /// If the value is set, it must be greater than zero. If the value is greater
     361             :   /// than 1, [maxLines] should also be set to either null or greater than
     362             :   /// this value.
     363             :   ///
     364             :   /// When [maxLines] is set as well, the width will grow between the indicated
     365             :   /// range of lines. When [maxLines] is null, it will grow as wide as needed,
     366             :   /// starting from [minLines].
     367             :   ///
     368             :   /// A few examples of behaviors possible with [minLines] and [maxLines] are as follows.
     369             :   /// These apply equally to `MongolTextField`, `MongolTextFormField`,
     370             :   /// and `MongolEditableText`.
     371             :   ///
     372             :   /// Input that always occupies at least 2 lines and has an infinite max.
     373             :   /// Expands horizontally as needed.
     374             :   /// ```dart
     375             :   /// MongolTextField(minLines: 2)
     376             :   /// ```
     377             :   ///
     378             :   /// Input whose width starts from 2 lines and grows up to 4 lines at which
     379             :   /// point the width limit is reached. If additional lines are entered it will
     380             :   /// scroll horizontally.
     381             :   /// ```dart
     382             :   /// MongolTextField(minLines:2, maxLines: 4)
     383             :   /// ```
     384             :   ///
     385             :   /// See the examples in [maxLines] for the complete picture of how [maxLines]
     386             :   /// and [minLines] interact to produce various behaviors.
     387             :   ///
     388             :   /// Defaults to null.
     389             :   final int? minLines;
     390             : 
     391             :   /// Whether this widget's width will be sized to fill its parent.
     392             :   ///
     393             :   /// If set to true and wrapped in a parent widget like [Expanded] or
     394             :   /// [SizedBox], the input will expand to fill the parent.
     395             :   ///
     396             :   /// [maxLines] and [minLines] must both be null when this is set to true,
     397             :   /// otherwise an error is thrown.
     398             :   ///
     399             :   /// Defaults to false.
     400             :   ///
     401             :   /// See the examples in [maxLines] for the complete picture of how [maxLines],
     402             :   /// [minLines], and [expands] interact to produce various behaviors.
     403             :   ///
     404             :   /// Input that matches the width of its parent:
     405             :   /// ```dart
     406             :   /// Expanded(
     407             :   ///   child: MongolTextField(maxLines: null, expands: true),
     408             :   /// )
     409             :   /// ```
     410             :   final bool expands;
     411             : 
     412             :   /// Whether this text field should focus itself if nothing else is already
     413             :   /// focused.
     414             :   ///
     415             :   /// If true, the keyboard will open as soon as this text field obtains focus.
     416             :   /// Otherwise, the keyboard is only shown after the user taps the text field.
     417             :   ///
     418             :   /// Defaults to false. Cannot be null.
     419             :   // See https://github.com/flutter/flutter/issues/7035 for the rationale for this
     420             :   // keyboard behavior.
     421             :   final bool autofocus;
     422             : 
     423             :   /// The color to use when painting the selection.
     424             :   ///
     425             :   /// For [MongolTextField]s, the value is set to the ambient
     426             :   /// [ThemeData.textSelectionColor].
     427             :   final Color? selectionColor;
     428             : 
     429             :   /// Optional delegate for building the text selection handles and toolbar.
     430             :   ///
     431             :   /// The [MongolEditableText] widget used on its own will not trigger the display
     432             :   /// of the selection toolbar by itself. The toolbar is shown by calling
     433             :   /// [EditableTextState.showToolbar] in response to an appropriate user event.
     434             :   ///
     435             :   /// See also:
     436             :   ///
     437             :   ///  * [MongolTextField], a Material Design themed wrapper of
     438             :   ///    [MongolEditableText], which shows the selection toolbar upon
     439             :   ///    appropriate user events based on the user's platform set in
     440             :   ///    [ThemeData.platform].
     441             :   final TextSelectionControls? selectionControls;
     442             : 
     443             :   /// The type of keyboard to use for editing the text.
     444             :   ///
     445             :   /// Defaults to [TextInputType.text] if [maxLines] is one and
     446             :   /// [TextInputType.multiline] otherwise.
     447             :   final TextInputType keyboardType;
     448             : 
     449             :   /// The type of action button to use with the soft keyboard.
     450             :   final TextInputAction? textInputAction;
     451             : 
     452             :   /// Called when the user initiates a change to the MongolTextField's
     453             :   /// value: when they have inserted or deleted text.
     454             :   ///
     455             :   /// This callback doesn't run when the MongolTextField's text is changed
     456             :   /// programmatically, via the MongolTextField's [controller]. Typically it
     457             :   /// isn't necessary to be notified of such changes, since they're
     458             :   /// initiated by the app itself.
     459             :   ///
     460             :   /// To be notified of all changes to the MongolTextField's text, cursor,
     461             :   /// and selection, one can add a listener to its [controller] with
     462             :   /// [TextEditingController.addListener].
     463             :   ///
     464             :   /// {@tool dartpad --template=stateful_widget_material}
     465             :   ///
     466             :   /// This example shows how onChanged could be used to check the MongolTextField's
     467             :   /// current value each time the user inserts or deletes a character.
     468             :   ///
     469             :   /// ```dart
     470             :   /// // TODO: test this snippet to make sure it works
     471             :   ///
     472             :   /// final TextEditingController _controller = TextEditingController();
     473             :   ///
     474             :   /// void dispose() {
     475             :   ///   _controller.dispose();
     476             :   ///   super.dispose();
     477             :   /// }
     478             :   ///
     479             :   /// Widget build(BuildContext context) {
     480             :   ///   return Scaffold(
     481             :   ///     body: Row(
     482             :   ///       mainAxisAlignment: MainAxisAlignment.center,
     483             :   ///       children: <Widget>[
     484             :   ///         const MongolText('What number comes next in the sequence?'),
     485             :   ///         const MongolText('1, 1, 2, 3, 5, 8...?'),
     486             :   ///         MongolTextField(
     487             :   ///           controller: _controller,
     488             :   ///           onChanged: (String value) async {
     489             :   ///             if (value != '13') {
     490             :   ///               return;
     491             :   ///             }
     492             :   ///             await showDialog<void>(
     493             :   ///               context: context,
     494             :   ///               builder: (BuildContext context) {
     495             :   ///                 return MongolAlertDialog(
     496             :   ///                   title: const Text('That is correct!'),
     497             :   ///                   content: Text ('13 is the right answer.'),
     498             :   ///                   actions: <Widget>[
     499             :   ///                     MongolTextButton(
     500             :   ///                       onPressed: () { Navigator.pop(context); },
     501             :   ///                       child: const Text('OK'),
     502             :   ///                     ),
     503             :   ///                   ],
     504             :   ///                 );
     505             :   ///               },
     506             :   ///             );
     507             :   ///           },
     508             :   ///         ),
     509             :   ///       ],
     510             :   ///     ),
     511             :   ///   );
     512             :   /// }
     513             :   /// ```
     514             :   /// {@end-tool}
     515             :   ///
     516             :   /// ## Handling emojis and other complex characters
     517             :   ///
     518             :   /// It's important to always use
     519             :   /// [characters](https://pub.dev/packages/characters) when dealing with user
     520             :   /// input text that may contain complex characters. This will ensure that
     521             :   /// extended grapheme clusters and surrogate pairs are treated as single
     522             :   /// characters, as they appear to the user.
     523             :   ///
     524             :   /// For example, when finding the length of some user input, use
     525             :   /// `string.characters.length`. Do NOT use `string.length` or even
     526             :   /// `string.runes.length`. For the complex character "👨‍👩‍👦", this
     527             :   /// appears to the user as a single character, and `string.characters.length`
     528             :   /// intuitively returns 1. On the other hand, `string.length` returns 8, and
     529             :   /// `string.runes.length` returns 5!
     530             :   ///
     531             :   /// See also:
     532             :   ///
     533             :   ///  * [inputFormatters], which are called before [onChanged]
     534             :   ///    runs and can validate and change ("format") the input value.
     535             :   ///  * [onEditingComplete], [onSubmitted], [onSelectionChanged]:
     536             :   ///    which are more specialized input change notifications.
     537             :   final ValueChanged<String>? onChanged;
     538             : 
     539             :   /// Called when the user submits editable content (e.g., user presses the "done"
     540             :   /// button on the keyboard).
     541             :   ///
     542             :   /// The default implementation of [onEditingComplete] executes 2 different
     543             :   /// behaviors based on the situation:
     544             :   ///
     545             :   ///  - When a completion action is pressed, such as "done", "go", "send", or
     546             :   ///    "search", the user's content is submitted to the [controller] and then
     547             :   ///    focus is given up.
     548             :   ///
     549             :   ///  - When a non-completion action is pressed, such as "next" or "previous",
     550             :   ///    the user's content is submitted to the [controller], but focus is not
     551             :   ///    given up because developers may want to immediately move focus to
     552             :   ///    another input widget within [onSubmitted].
     553             :   ///
     554             :   /// Providing [onEditingComplete] prevents the aforementioned default behavior.
     555             :   final VoidCallback? onEditingComplete;
     556             : 
     557             :   /// Called when the user indicates that they are done editing the text in the
     558             :   /// field.
     559             :   final ValueChanged<String>? onSubmitted;
     560             : 
     561             :   /// This is used to receive a private command from the input method.
     562             :   ///
     563             :   /// Called when the result of [TextInputClient.performPrivateCommand] is
     564             :   /// received.
     565             :   ///
     566             :   /// This can be used to provide domain-specific features that are only known
     567             :   /// between certain input methods and their clients.
     568             :   ///
     569             :   /// See also:
     570             :   ///   * [https://developer.android.com/reference/android/view/inputmethod/InputConnection#performPrivateCommand(java.lang.String,%20android.os.Bundle)],
     571             :   ///     which is the Android documentation for performPrivateCommand, used to
     572             :   ///     send a command from the input method.
     573             :   ///   * [https://developer.android.com/reference/android/view/inputmethod/InputMethodManager#sendAppPrivateCommand],
     574             :   ///     which is the Android documentation for sendAppPrivateCommand, used to
     575             :   ///     send a command to the input method.
     576             :   final AppPrivateCommandCallback? onAppPrivateCommand;
     577             : 
     578             :   /// Called when the user changes the selection of text (including the cursor
     579             :   /// location).
     580             :   final SelectionChangedCallback? onSelectionChanged;
     581             : 
     582             :   /// A callback that's invoked when a selection handle is tapped.
     583             :   ///
     584             :   /// Both regular taps and long presses invoke this callback, but a drag
     585             :   /// gesture won't.
     586             :   final VoidCallback? onSelectionHandleTapped;
     587             : 
     588             :   /// Optional input validation and formatting overrides.
     589             :   ///
     590             :   /// Formatters are run in the provided order when the text input changes. When
     591             :   /// this parameter changes, the new formatters will not be applied until the
     592             :   /// next time the user inserts or deletes text.
     593             :   final List<TextInputFormatter>? inputFormatters;
     594             : 
     595             :   /// The cursor for a mouse pointer when it enters or is hovering over the
     596             :   /// widget.
     597             :   ///
     598             :   /// If this property is null, [SystemMouseCursors.text] will be used.
     599             :   ///
     600             :   /// The [mouseCursor] is the only property of [MongolEditableText] that controls the
     601             :   /// appearance of the mouse pointer. All other properties related to "cursor"
     602             :   /// stands for the text cursor, which is usually a blinking vertical line at
     603             :   /// the editing position.
     604             :   final MouseCursor? mouseCursor;
     605             : 
     606             :   /// If true, the [MongolRenderEditable] created by this widget will not handle
     607             :   /// pointer events, see [MongolRenderEditable] and
     608             :   /// [MongolRenderEditable.ignorePointer].
     609             :   ///
     610             :   /// This property is false by default.
     611             :   final bool rendererIgnoresPointer;
     612             : 
     613             :   /// How wide the cursor will be.
     614             :   ///
     615             :   /// If this property is null, [MongolRenderEditable.preferredLineWidth] will
     616             :   /// be used.
     617             :   final double? cursorWidth;
     618             : 
     619             :   /// How thick the cursor will be.
     620             :   ///
     621             :   /// Defaults to 2.0.
     622             :   ///
     623             :   /// The cursor will draw above the text. The cursor height will extend
     624             :   /// down from the boundary between characters. This corresponds to extending
     625             :   /// downstream relative to the selected position. Negative values may be used
     626             :   /// to reverse this behavior.
     627             :   final double cursorHeight;
     628             : 
     629             :   /// How rounded the corners of the cursor should be.
     630             :   ///
     631             :   /// By default, the cursor has no radius.
     632             :   final Radius? cursorRadius;
     633             : 
     634             :   /// Whether the cursor will animate from fully transparent to fully opaque
     635             :   /// during each cursor blink.
     636             :   ///
     637             :   /// By default, the cursor opacity will animate on iOS platforms and will not
     638             :   /// animate on Android platforms.
     639             :   ///
     640             :   // TODO: can we remove this or use one setting for both platforms?
     641             :   final bool cursorOpacityAnimates;
     642             : 
     643             :   /// The offset that is used, in pixels, when painting the cursor on screen.
     644             :   ///
     645             :   /// By default, the cursor position should be set to an offset of
     646             :   /// (0.0, -[cursorHeight] * 0.5) on iOS platforms and (0, 0) on Android
     647             :   /// platforms. The origin from where the offset is applied to is the arbitrary
     648             :   /// location where the cursor ends up being rendered from by default.
     649             :   final Offset? cursorOffset;
     650             : 
     651             :   /// The appearance of the keyboard.
     652             :   ///
     653             :   /// This setting is only honored on iOS devices.
     654             :   ///
     655             :   /// Defaults to [Brightness.light].
     656             :   final Brightness keyboardAppearance;
     657             : 
     658             :   /// Configures padding to edges surrounding a [Scrollable] when the
     659             :   /// MongolTextField scrolls into view.
     660             :   ///
     661             :   /// When this widget receives focus and is not completely visible (for
     662             :   /// example scrolled partially off the screen or overlapped by the keyboard)
     663             :   /// then it will attempt to make itself visible by scrolling a surrounding
     664             :   /// [Scrollable], if one is present. This value controls how far from the
     665             :   /// edges of a [Scrollable] the MongolTextField will be positioned after the
     666             :   /// scroll.
     667             :   ///
     668             :   /// Defaults to EdgeInsets.all(20.0).
     669             :   final EdgeInsets scrollPadding;
     670             : 
     671             :   /// Whether to enable user interface affordances for changing the
     672             :   /// text selection.
     673             :   ///
     674             :   /// For example, setting this to true will enable features such as
     675             :   /// long-pressing the MongolTextField to select text and show the
     676             :   /// cut/copy/paste menu, and tapping to move the text caret.
     677             :   ///
     678             :   /// When this is false, the text selection cannot be adjusted by
     679             :   /// the user, text cannot be copied, and the user cannot paste into
     680             :   /// the text field from the clipboard.
     681             :   final bool enableInteractiveSelection;
     682             : 
     683             :   /// Setting this property to true makes the cursor stop blinking or fading
     684             :   /// on and off once the cursor appears on focus. This property is useful for
     685             :   /// testing purposes.
     686             :   ///
     687             :   /// It does not affect the necessity to focus the EditableText for the cursor
     688             :   /// to appear in the first place.
     689             :   ///
     690             :   /// Defaults to false, resulting in a typical blinking cursor.
     691             :   static bool debugDeterministicCursor = false;
     692             : 
     693             :   /// Determines the way that drag start behavior is handled.
     694             :   ///
     695             :   /// If set to [DragStartBehavior.start], scrolling drag behavior will
     696             :   /// begin upon the detection of a drag gesture. If set to
     697             :   /// [DragStartBehavior.down] it will begin when a down event is first detected.
     698             :   ///
     699             :   /// In general, setting this to [DragStartBehavior.start] will make drag
     700             :   /// animation smoother and setting it to [DragStartBehavior.down] will make
     701             :   /// drag behavior feel slightly more reactive.
     702             :   ///
     703             :   /// By default, the drag start behavior is [DragStartBehavior.start].
     704             :   ///
     705             :   /// See also:
     706             :   ///
     707             :   ///  * [DragGestureRecognizer.dragStartBehavior], which gives an example for
     708             :   ///    the different behaviors.
     709             :   final DragStartBehavior dragStartBehavior;
     710             : 
     711             :   /// The [ScrollController] to use when horizontally scrolling the input.
     712             :   ///
     713             :   /// If null, it will instantiate a new ScrollController.
     714             :   ///
     715             :   /// See [Scrollable.controller].
     716             :   final ScrollController? scrollController;
     717             : 
     718             :   /// The [ScrollPhysics] to use when horizontally scrolling the input.
     719             :   ///
     720             :   /// If not specified, it will behave according to the current platform.
     721             :   ///
     722             :   /// See [Scrollable.physics].
     723             :   ///
     724             :   /// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the
     725             :   /// [ScrollPhysics] provided by that behavior will take precedence after
     726             :   /// [scrollPhysics].
     727             :   final ScrollPhysics? scrollPhysics;
     728             : 
     729             :   /// Same as [enableInteractiveSelection].
     730             :   ///
     731             :   /// This getter exists primarily for consistency with
     732             :   /// [MongolRenderEditable.selectionEnabled].
     733           4 :   bool get selectionEnabled => enableInteractiveSelection;
     734             : 
     735             :   /// A list of strings that helps the autofill service identify the type of this
     736             :   /// text input.
     737             :   ///
     738             :   /// When set to null or empty, this text input will not send its autofill
     739             :   /// information to the platform, preventing it from participating in
     740             :   /// autofills triggered by a different [AutofillClient], even if they're in the
     741             :   /// same [AutofillScope]. Additionally, on Android and web, setting this to
     742             :   /// null or empty will disable autofill for this text field.
     743             :   ///
     744             :   /// The minimum platform SDK version that supports Autofill is API level 26
     745             :   /// for Android, and iOS 10.0 for iOS.
     746             :   ///
     747             :   /// ### Setting up iOS autofill:
     748             :   ///
     749             :   /// To provide the best user experience and ensure your app fully supports
     750             :   /// password autofill on iOS, follow these steps:
     751             :   ///
     752             :   /// * Set up your iOS app's
     753             :   ///   [associated domains](https://developer.apple.com/documentation/safariservices/supporting_associated_domains_in_your_app).
     754             :   /// * Some autofill hints only work with specific [keyboardType]s. For example,
     755             :   ///   [AutofillHints.name] requires [TextInputType.name] and [AutofillHints.email]
     756             :   ///   works only with [TextInputType.emailAddress]. Make sure the input field has a
     757             :   ///   compatible [keyboardType]. Empirically, [TextInputType.name] works well
     758             :   ///   with many autofill hints that are predefined on iOS.
     759             :   ///
     760             :   /// ### Troubleshooting Autofill
     761             :   ///
     762             :   /// Autofill service providers rely heavily on [autofillHints]. Make sure the
     763             :   /// entries in [autofillHints] are supported by the autofill service currently
     764             :   /// in use (the name of the service can typically be found in your mobile
     765             :   /// device's system settings).
     766             :   ///
     767             :   /// #### Autofill UI refuses to show up when I tap on the text field
     768             :   ///
     769             :   /// Check the device's system settings and make sure autofill is turned on,
     770             :   /// and there're available credentials stored in the autofill service.
     771             :   ///
     772             :   /// * iOS password autofill: Go to Settings -> Password, turn on "Autofill
     773             :   ///   Passwords", and add new passwords for testing by pressing the top right
     774             :   ///   "+" button. Use an arbitrary "website" if you don't have associated
     775             :   ///   domains set up for your app. As long as there's at least one password
     776             :   ///   stored, you should be able to see a key-shaped icon in the quick type
     777             :   ///   bar on the software keyboard, when a password related field is focused.
     778             :   ///
     779             :   /// * iOS contact information autofill: iOS seems to pull contact info from
     780             :   ///   the Apple ID currently associated with the device. Go to Settings ->
     781             :   ///   Apple ID (usually the first entry, or "Sign in to your iPhone" if you
     782             :   ///   haven't set up one on the device), and fill out the relevant fields. If
     783             :   ///   you wish to test more contact info types, try adding them in Contacts ->
     784             :   ///   My Card.
     785             :   ///
     786             :   /// * Android autofill: Go to Settings -> System -> Languages & input ->
     787             :   ///   Autofill service. Enable the autofill service of your choice, and make
     788             :   ///   sure there're available credentials associated with your app.
     789             :   ///
     790             :   /// #### I called `TextInput.finishAutofillContext` but the autofill save
     791             :   /// prompt isn't showing
     792             :   ///
     793             :   /// * iOS: iOS may not show a prompt or any other visual indication when it
     794             :   ///   saves user password. Go to Settings -> Password and check if your new
     795             :   ///   password is saved. Neither saving password nor auto-generating strong
     796             :   ///   password works without properly setting up associated domains in your
     797             :   ///   app. To set up associated domains, follow the instructions in
     798             :   ///   <https://developer.apple.com/documentation/safariservices/supporting_associated_domains_in_your_app>.
     799             :   final Iterable<String>? autofillHints;
     800             : 
     801             :   /// The content will be clipped (or not) according to this option.
     802             :   ///
     803             :   /// See the enum [Clip] for details of all possible options and their common
     804             :   /// use cases.
     805             :   ///
     806             :   /// Defaults to [Clip.hardEdge].
     807             :   final Clip clipBehavior;
     808             : 
     809             :   /// Restoration ID to save and restore the scroll offset of the
     810             :   /// [MongolEditableText].
     811             :   ///
     812             :   /// If a restoration id is provided, the [MongolEditableText] will persist its
     813             :   /// current scroll offset and restore it during state restoration.
     814             :   ///
     815             :   /// The scroll offset is persisted in a [RestorationBucket] claimed from
     816             :   /// the surrounding [RestorationScope] using the provided restoration ID.
     817             :   ///
     818             :   /// Persisting and restoring the content of the [MongolEditableText] is the
     819             :   /// responsibility of the owner of the [controller], who may use a
     820             :   /// [RestorableTextEditingController] for that purpose.
     821             :   ///
     822             :   /// See also:
     823             :   ///
     824             :   ///  * [RestorationManager], which explains how state restoration works in
     825             :   ///    Flutter.
     826             :   final String? restorationId;
     827             : 
     828             :   /// A [ScrollBehavior] that will be applied to this widget individually.
     829             :   ///
     830             :   /// Defaults to null, wherein the inherited [ScrollBehavior] is copied and
     831             :   /// modified to alter the viewport decoration, like [Scrollbar]s.
     832             :   ///
     833             :   /// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit
     834             :   /// [ScrollPhysics] is provided in [scrollPhysics], it will take precedence,
     835             :   /// followed by [scrollBehavior], and then the inherited ancestor
     836             :   /// [ScrollBehavior].
     837             :   ///
     838             :   /// The [ScrollBehavior] of the inherited [ScrollConfiguration] will be
     839             :   /// modified by default to only apply a [Scrollbar] if [maxLines] is greater
     840             :   /// than 1.
     841             :   final ScrollBehavior? scrollBehavior;
     842             : 
     843             :   // Infer the keyboard type of a `MongolEditableText` if it's not specified.
     844           1 :   static TextInputType _inferKeyboardType({
     845             :     required Iterable<String>? autofillHints,
     846             :     required int? maxLines,
     847             :   }) {
     848           1 :     if (autofillHints?.isEmpty ?? true) {
     849           1 :       return maxLines == 1 ? TextInputType.text : TextInputType.multiline;
     850             :     }
     851             : 
     852             :     TextInputType? returnValue;
     853           1 :     final effectiveHint = autofillHints!.first;
     854             : 
     855             :     // On iOS oftentimes specifying a text content type is not enough to qualify
     856             :     // the input field for autofill. The keyboard type also needs to be compatible
     857             :     // with the content type. To get autofill to work by default on MongolEditableText,
     858             :     // the keyboard type inference on iOS is done differently from other platforms.
     859             :     //
     860             :     // The entries with "autofill not working" comments are the iOS text content
     861             :     // types that should work with the specified keyboard type but won't trigger
     862             :     // (even within a native app). Tested on iOS 13.5.
     863             :     if (!kIsWeb) {
     864           1 :       switch (defaultTargetPlatform) {
     865           1 :         case TargetPlatform.iOS:
     866           1 :         case TargetPlatform.macOS:
     867             :           const iOSKeyboardType = <String, TextInputType>{
     868             :             AutofillHints.addressCity: TextInputType.name,
     869             :             AutofillHints.addressCityAndState:
     870             :                 TextInputType.name, // Autofill not working.
     871             :             AutofillHints.addressState: TextInputType.name,
     872             :             AutofillHints.countryName: TextInputType.name,
     873             :             AutofillHints.creditCardNumber:
     874             :                 TextInputType.number, // Couldn't test.
     875             :             AutofillHints.email: TextInputType.emailAddress,
     876             :             AutofillHints.familyName: TextInputType.name,
     877             :             AutofillHints.fullStreetAddress: TextInputType.name,
     878             :             AutofillHints.givenName: TextInputType.name,
     879             :             AutofillHints.jobTitle: TextInputType.name, // Autofill not working.
     880             :             AutofillHints.location: TextInputType.name, // Autofill not working.
     881             :             AutofillHints.middleName:
     882             :                 TextInputType.name, // Autofill not working.
     883             :             AutofillHints.name: TextInputType.name,
     884             :             AutofillHints.namePrefix:
     885             :                 TextInputType.name, // Autofill not working.
     886             :             AutofillHints.nameSuffix:
     887             :                 TextInputType.name, // Autofill not working.
     888             :             AutofillHints.newPassword: TextInputType.text,
     889             :             AutofillHints.newUsername: TextInputType.text,
     890             :             AutofillHints.nickname: TextInputType.name, // Autofill not working.
     891             :             AutofillHints.oneTimeCode: TextInputType.number,
     892             :             AutofillHints.organizationName:
     893             :                 TextInputType.text, // Autofill not working.
     894             :             AutofillHints.password: TextInputType.text,
     895             :             AutofillHints.postalCode: TextInputType.name,
     896             :             AutofillHints.streetAddressLine1: TextInputType.name,
     897             :             AutofillHints.streetAddressLine2:
     898             :                 TextInputType.name, // Autofill not working.
     899             :             AutofillHints.sublocality:
     900             :                 TextInputType.name, // Autofill not working.
     901             :             AutofillHints.telephoneNumber: TextInputType.name,
     902             :             AutofillHints.url: TextInputType.url, // Autofill not working.
     903             :             AutofillHints.username: TextInputType.text,
     904             :           };
     905             : 
     906           1 :           returnValue = iOSKeyboardType[effectiveHint];
     907             :           break;
     908           1 :         case TargetPlatform.android:
     909           0 :         case TargetPlatform.fuchsia:
     910           0 :         case TargetPlatform.linux:
     911           0 :         case TargetPlatform.windows:
     912             :           break;
     913             :       }
     914             :     }
     915             : 
     916           1 :     if (returnValue != null || maxLines != 1) {
     917             :       return returnValue ?? TextInputType.multiline;
     918             :     }
     919             : 
     920             :     const inferKeyboardType = <String, TextInputType>{
     921             :       AutofillHints.addressCity: TextInputType.streetAddress,
     922             :       AutofillHints.addressCityAndState: TextInputType.streetAddress,
     923             :       AutofillHints.addressState: TextInputType.streetAddress,
     924             :       AutofillHints.birthday: TextInputType.datetime,
     925             :       AutofillHints.birthdayDay: TextInputType.datetime,
     926             :       AutofillHints.birthdayMonth: TextInputType.datetime,
     927             :       AutofillHints.birthdayYear: TextInputType.datetime,
     928             :       AutofillHints.countryCode: TextInputType.number,
     929             :       AutofillHints.countryName: TextInputType.text,
     930             :       AutofillHints.creditCardExpirationDate: TextInputType.datetime,
     931             :       AutofillHints.creditCardExpirationDay: TextInputType.datetime,
     932             :       AutofillHints.creditCardExpirationMonth: TextInputType.datetime,
     933             :       AutofillHints.creditCardExpirationYear: TextInputType.datetime,
     934             :       AutofillHints.creditCardFamilyName: TextInputType.name,
     935             :       AutofillHints.creditCardGivenName: TextInputType.name,
     936             :       AutofillHints.creditCardMiddleName: TextInputType.name,
     937             :       AutofillHints.creditCardName: TextInputType.name,
     938             :       AutofillHints.creditCardNumber: TextInputType.number,
     939             :       AutofillHints.creditCardSecurityCode: TextInputType.number,
     940             :       AutofillHints.creditCardType: TextInputType.text,
     941             :       AutofillHints.email: TextInputType.emailAddress,
     942             :       AutofillHints.familyName: TextInputType.name,
     943             :       AutofillHints.fullStreetAddress: TextInputType.streetAddress,
     944             :       AutofillHints.gender: TextInputType.text,
     945             :       AutofillHints.givenName: TextInputType.name,
     946             :       AutofillHints.impp: TextInputType.url,
     947             :       AutofillHints.jobTitle: TextInputType.text,
     948             :       AutofillHints.language: TextInputType.text,
     949             :       AutofillHints.location: TextInputType.streetAddress,
     950             :       AutofillHints.middleInitial: TextInputType.name,
     951             :       AutofillHints.middleName: TextInputType.name,
     952             :       AutofillHints.name: TextInputType.name,
     953             :       AutofillHints.namePrefix: TextInputType.name,
     954             :       AutofillHints.nameSuffix: TextInputType.name,
     955             :       AutofillHints.newPassword: TextInputType.text,
     956             :       AutofillHints.newUsername: TextInputType.text,
     957             :       AutofillHints.nickname: TextInputType.text,
     958             :       AutofillHints.oneTimeCode: TextInputType.text,
     959             :       AutofillHints.organizationName: TextInputType.text,
     960             :       AutofillHints.password: TextInputType.text,
     961             :       AutofillHints.photo: TextInputType.text,
     962             :       AutofillHints.postalAddress: TextInputType.streetAddress,
     963             :       AutofillHints.postalAddressExtended: TextInputType.streetAddress,
     964             :       AutofillHints.postalAddressExtendedPostalCode: TextInputType.number,
     965             :       AutofillHints.postalCode: TextInputType.number,
     966             :       AutofillHints.streetAddressLevel1: TextInputType.streetAddress,
     967             :       AutofillHints.streetAddressLevel2: TextInputType.streetAddress,
     968             :       AutofillHints.streetAddressLevel3: TextInputType.streetAddress,
     969             :       AutofillHints.streetAddressLevel4: TextInputType.streetAddress,
     970             :       AutofillHints.streetAddressLine1: TextInputType.streetAddress,
     971             :       AutofillHints.streetAddressLine2: TextInputType.streetAddress,
     972             :       AutofillHints.streetAddressLine3: TextInputType.streetAddress,
     973             :       AutofillHints.sublocality: TextInputType.streetAddress,
     974             :       AutofillHints.telephoneNumber: TextInputType.phone,
     975             :       AutofillHints.telephoneNumberAreaCode: TextInputType.phone,
     976             :       AutofillHints.telephoneNumberCountryCode: TextInputType.phone,
     977             :       AutofillHints.telephoneNumberDevice: TextInputType.phone,
     978             :       AutofillHints.telephoneNumberExtension: TextInputType.phone,
     979             :       AutofillHints.telephoneNumberLocal: TextInputType.phone,
     980             :       AutofillHints.telephoneNumberLocalPrefix: TextInputType.phone,
     981             :       AutofillHints.telephoneNumberLocalSuffix: TextInputType.phone,
     982             :       AutofillHints.telephoneNumberNational: TextInputType.phone,
     983             :       AutofillHints.transactionAmount:
     984             :           TextInputType.numberWithOptions(decimal: true),
     985             :       AutofillHints.transactionCurrency: TextInputType.text,
     986             :       AutofillHints.url: TextInputType.url,
     987             :       AutofillHints.username: TextInputType.text,
     988             :     };
     989             : 
     990           1 :     return inferKeyboardType[effectiveHint] ?? TextInputType.text;
     991             :   }
     992             : 
     993           2 :   @override
     994           2 :   MongolEditableTextState createState() => MongolEditableTextState();
     995             : 
     996           0 :   @override
     997             :   void debugFillProperties(DiagnosticPropertiesBuilder properties) {
     998           0 :     super.debugFillProperties(properties);
     999           0 :     properties.add(
    1000           0 :         DiagnosticsProperty<TextEditingController>('controller', controller));
    1001           0 :     properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode));
    1002           0 :     properties.add(DiagnosticsProperty<bool>('obscureText', obscureText,
    1003             :         defaultValue: false));
    1004           0 :     properties.add(DiagnosticsProperty<bool>('autocorrect', autocorrect,
    1005             :         defaultValue: true));
    1006           0 :     properties.add(DiagnosticsProperty<bool>(
    1007           0 :         'enableSuggestions', enableSuggestions,
    1008             :         defaultValue: true));
    1009           0 :     style.debugFillProperties(properties);
    1010           0 :     properties.add(EnumProperty<MongolTextAlign>('textAlign', textAlign,
    1011             :         defaultValue: null));
    1012           0 :     properties.add(
    1013           0 :         DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: null));
    1014           0 :     properties.add(IntProperty('maxLines', maxLines, defaultValue: 1));
    1015           0 :     properties.add(IntProperty('minLines', minLines, defaultValue: null));
    1016           0 :     properties.add(
    1017           0 :         DiagnosticsProperty<bool>('expands', expands, defaultValue: false));
    1018           0 :     properties.add(
    1019           0 :         DiagnosticsProperty<bool>('autofocus', autofocus, defaultValue: false));
    1020           0 :     properties.add(DiagnosticsProperty<TextInputType>(
    1021           0 :         'keyboardType', keyboardType,
    1022             :         defaultValue: null));
    1023           0 :     properties.add(DiagnosticsProperty<ScrollController>(
    1024           0 :         'scrollController', scrollController,
    1025             :         defaultValue: null));
    1026           0 :     properties.add(DiagnosticsProperty<ScrollPhysics>(
    1027           0 :         'scrollPhysics', scrollPhysics,
    1028             :         defaultValue: null));
    1029           0 :     properties.add(DiagnosticsProperty<Iterable<String>>(
    1030           0 :         'autofillHints', autofillHints,
    1031             :         defaultValue: null));
    1032             :   }
    1033             : }
    1034             : 
    1035             : /// State for a [MongolEditableText].
    1036             : class MongolEditableTextState extends State<MongolEditableText>
    1037             :     with
    1038             :         AutomaticKeepAliveClientMixin<MongolEditableText>,
    1039             :         WidgetsBindingObserver,
    1040             :         TickerProviderStateMixin<MongolEditableText>,
    1041             :         TextSelectionDelegate
    1042             :     implements TextInputClient, AutofillClient, MongolTextEditingActionTarget {
    1043             :   Timer? _cursorTimer;
    1044             :   bool _targetCursorVisibility = false;
    1045             :   final ValueNotifier<bool> _cursorVisibilityNotifier =
    1046             :       ValueNotifier<bool>(true);
    1047             :   final GlobalKey _editableKey = GlobalKey();
    1048             :   final ClipboardStatusNotifier? _clipboardStatus =
    1049             :       kIsWeb ? null : ClipboardStatusNotifier();
    1050             : 
    1051             :   TextInputConnection? _textInputConnection;
    1052             :   MongolTextSelectionOverlay? _selectionOverlay;
    1053             : 
    1054             :   ScrollController? _scrollController;
    1055             : 
    1056             :   late AnimationController _cursorBlinkOpacityController;
    1057             : 
    1058             :   final LayerLink _toolbarLayerLink = LayerLink();
    1059             :   final LayerLink _startHandleLayerLink = LayerLink();
    1060             :   final LayerLink _endHandleLayerLink = LayerLink();
    1061             : 
    1062             :   bool _didAutoFocus = false;
    1063             :   FocusAttachment? _focusAttachment;
    1064             : 
    1065             :   AutofillGroupState? _currentAutofillScope;
    1066           2 :   @override
    1067           2 :   AutofillScope? get currentAutofillScope => _currentAutofillScope;
    1068             : 
    1069             :   // Is this field in the current autofill context.
    1070             :   bool _isInAutofillContext = false;
    1071             : 
    1072             :   /// Whether to create an input connection with the platform for text editing
    1073             :   /// or not.
    1074             :   ///
    1075             :   /// Read-only input fields do not need a connection with the platform since
    1076             :   /// there's no need for text editing capabilities (e.g. virtual keyboard).
    1077             :   ///
    1078             :   /// On the web, we always need a connection because we want some browser
    1079             :   /// functionalities to continue to work on read-only input fields like:
    1080             :   ///
    1081             :   /// - Relevant context menu.
    1082             :   /// - cmd/ctrl+c shortcut to copy.
    1083             :   /// - cmd/ctrl+a to select all.
    1084             :   /// - Changing the selection using a physical keyboard.
    1085           6 :   bool get _shouldCreateInputConnection => kIsWeb || !widget.readOnly;
    1086             : 
    1087             :   // This value is an eyeball estimation of the time it takes for the iOS cursor
    1088             :   // to ease in and out.
    1089             :   static const Duration _fadeDuration = Duration(milliseconds: 250);
    1090             : 
    1091           2 :   @override
    1092           6 :   bool get wantKeepAlive => widget.focusNode.hasFocus;
    1093             : 
    1094           2 :   Color get _cursorColor =>
    1095          10 :       widget.cursorColor.withOpacity(_cursorBlinkOpacityController.value);
    1096             : 
    1097           2 :   @override
    1098          10 :   bool get cutEnabled => widget.toolbarOptions.cut && !widget.readOnly;
    1099             : 
    1100           2 :   @override
    1101           6 :   bool get copyEnabled => widget.toolbarOptions.copy;
    1102             : 
    1103           2 :   @override
    1104          10 :   bool get pasteEnabled => widget.toolbarOptions.paste && !widget.readOnly;
    1105             : 
    1106           2 :   @override
    1107           6 :   bool get selectAllEnabled => widget.toolbarOptions.selectAll;
    1108             : 
    1109           2 :   void _onChangedClipboardStatus() {
    1110           4 :     setState(() {
    1111             :       // Inform the widget that the value of clipboardStatus has changed.
    1112             :     });
    1113             :   }
    1114             : 
    1115             :   // State lifecycle:
    1116             : 
    1117           2 :   @override
    1118             :   void initState() {
    1119           2 :     super.initState();
    1120           6 :     _clipboardStatus?.addListener(_onChangedClipboardStatus);
    1121           8 :     widget.controller.addListener(_didChangeTextEditingValue);
    1122          10 :     _focusAttachment = widget.focusNode.attach(context);
    1123           8 :     widget.focusNode.addListener(_handleFocusChanged);
    1124           8 :     _scrollController = widget.scrollController ?? ScrollController();
    1125           6 :     _scrollController!.addListener(() {
    1126           3 :       _selectionOverlay?.updateForScroll();
    1127             :     });
    1128           2 :     _cursorBlinkOpacityController =
    1129           2 :         AnimationController(vsync: this, duration: _fadeDuration);
    1130           6 :     _cursorBlinkOpacityController.addListener(_onCursorColorTick);
    1131           8 :     _cursorVisibilityNotifier.value = widget.showCursor;
    1132             :   }
    1133             : 
    1134           2 :   @override
    1135             :   void didChangeDependencies() {
    1136           2 :     super.didChangeDependencies();
    1137             : 
    1138           4 :     final newAutofillGroup = AutofillGroup.of(context);
    1139           4 :     if (currentAutofillScope != newAutofillGroup) {
    1140           0 :       _currentAutofillScope?.unregister(autofillId);
    1141           0 :       _currentAutofillScope = newAutofillGroup;
    1142           0 :       newAutofillGroup?.register(this);
    1143           0 :       _isInAutofillContext = _isInAutofillContext || _shouldBeInAutofillContext;
    1144             :     }
    1145             : 
    1146           6 :     if (!_didAutoFocus && widget.autofocus) {
    1147           2 :       _didAutoFocus = true;
    1148           6 :       SchedulerBinding.instance!.addPostFrameCallback((_) {
    1149           2 :         if (mounted) {
    1150          10 :           FocusScope.of(context).autofocus(widget.focusNode);
    1151             :         }
    1152             :       });
    1153             :     }
    1154             :   }
    1155             : 
    1156           2 :   @override
    1157             :   void didUpdateWidget(MongolEditableText oldWidget) {
    1158           2 :     super.didUpdateWidget(oldWidget);
    1159           8 :     if (widget.controller != oldWidget.controller) {
    1160           6 :       oldWidget.controller.removeListener(_didChangeTextEditingValue);
    1161           8 :       widget.controller.addListener(_didChangeTextEditingValue);
    1162           2 :       _updateRemoteEditingValueIfNeeded();
    1163             :     }
    1164          12 :     if (widget.controller.selection != oldWidget.controller.selection) {
    1165           6 :       _selectionOverlay?.update(_value);
    1166             :     }
    1167           8 :     _selectionOverlay?.handlesVisible = widget.showSelectionHandles;
    1168           6 :     _isInAutofillContext = _isInAutofillContext || _shouldBeInAutofillContext;
    1169             : 
    1170           8 :     if (widget.focusNode != oldWidget.focusNode) {
    1171           3 :       oldWidget.focusNode.removeListener(_handleFocusChanged);
    1172           2 :       _focusAttachment?.detach();
    1173           5 :       _focusAttachment = widget.focusNode.attach(context);
    1174           4 :       widget.focusNode.addListener(_handleFocusChanged);
    1175           1 :       updateKeepAlive();
    1176             :     }
    1177           2 :     if (!_shouldCreateInputConnection) {
    1178           1 :       _closeInputConnectionIfNeeded();
    1179             :     } else {
    1180           4 :       if (oldWidget.readOnly && _hasFocus) {
    1181           2 :         _openInputConnection();
    1182             :       }
    1183             :     }
    1184             : 
    1185           0 :     if (kIsWeb && _hasInputConnection) {
    1186           0 :       if (oldWidget.readOnly != widget.readOnly) {
    1187           0 :         _textInputConnection!.updateConfig(textInputConfiguration);
    1188             :       }
    1189             :     }
    1190             : 
    1191           8 :     if (widget.style != oldWidget.style) {
    1192           4 :       final style = widget.style;
    1193             :       // The _textInputConnection will pick up the new style when it attaches in
    1194             :       // _openInputConnection.
    1195           2 :       if (_hasInputConnection) {
    1196           2 :         _textInputConnection!.setStyle(
    1197           1 :           fontFamily: style.fontFamily,
    1198           1 :           fontSize: style.fontSize,
    1199           1 :           fontWeight: style.fontWeight,
    1200             :           textDirection: TextDirection.ltr,
    1201           3 :           textAlign: _rotatedTextAlign(widget.textAlign),
    1202             :         );
    1203             :       }
    1204             :     }
    1205           4 :     if (widget.selectionEnabled &&
    1206           2 :         pasteEnabled &&
    1207           8 :         widget.selectionControls?.canPaste(this) == true) {
    1208           4 :       _clipboardStatus?.update();
    1209             :     }
    1210             :   }
    1211             : 
    1212           2 :   TextAlign _rotatedTextAlign(MongolTextAlign mongolTextAlign) {
    1213             :     switch (mongolTextAlign) {
    1214           2 :       case MongolTextAlign.top:
    1215             :         return TextAlign.left;
    1216           1 :       case MongolTextAlign.center:
    1217             :         return ui.TextAlign.center;
    1218           1 :       case MongolTextAlign.bottom:
    1219             :         return TextAlign.right;
    1220           0 :       case MongolTextAlign.justify:
    1221             :         return TextAlign.justify;
    1222             :     }
    1223             :   }
    1224             : 
    1225           2 :   @override
    1226             :   void dispose() {
    1227           2 :     _currentAutofillScope?.unregister(autofillId);
    1228           8 :     widget.controller.removeListener(_didChangeTextEditingValue);
    1229           6 :     _cursorBlinkOpacityController.removeListener(_onCursorColorTick);
    1230           2 :     _closeInputConnectionIfNeeded();
    1231           2 :     assert(!_hasInputConnection);
    1232           2 :     _stopCursorTimer();
    1233           2 :     assert(_cursorTimer == null);
    1234           4 :     _selectionOverlay?.dispose();
    1235           2 :     _selectionOverlay = null;
    1236           4 :     _focusAttachment!.detach();
    1237           8 :     widget.focusNode.removeListener(_handleFocusChanged);
    1238           4 :     WidgetsBinding.instance!.removeObserver(this);
    1239           6 :     _clipboardStatus?.removeListener(_onChangedClipboardStatus);
    1240           4 :     _clipboardStatus?.dispose();
    1241           2 :     super.dispose();
    1242           6 :     assert(_batchEditDepth <= 0, 'unfinished batch edits: $_batchEditDepth');
    1243             :   }
    1244             : 
    1245             :   // TextInputClient implementation:
    1246             : 
    1247             :   /// The last known [TextEditingValue] of the platform text input plugin.
    1248             :   ///
    1249             :   /// This value is updated when the platform text input plugin sends a new
    1250             :   /// update via [updateEditingValue], or when [MongolEditableText] calls
    1251             :   /// [TextInputConnection.setEditingState] to overwrite the platform text input
    1252             :   /// plugin's [TextEditingValue].
    1253             :   ///
    1254             :   /// Used in [_updateRemoteEditingValueIfNeeded] to determine whether the
    1255             :   /// remote value is outdated and needs updating.
    1256             :   TextEditingValue? _lastKnownRemoteTextEditingValue;
    1257             : 
    1258           1 :   @override
    1259           1 :   TextEditingValue get currentTextEditingValue => _value;
    1260             : 
    1261           2 :   @override
    1262             :   void updateEditingValue(TextEditingValue value) {
    1263             :     // This method handles text editing state updates from the platform text
    1264             :     // input plugin. The [MongolEditableText] may not have the focus or an open input
    1265             :     // connection, as autofill can update a disconnected [MongolEditableText].
    1266             : 
    1267             :     // Since we still have to support keyboard select, this is the best place
    1268             :     // to disable text updating.
    1269           2 :     if (!_shouldCreateInputConnection) {
    1270             :       return;
    1271             :     }
    1272             : 
    1273           4 :     if (widget.readOnly) {
    1274             :       // In the read-only case, we only care about selection changes, and reject
    1275             :       // everything else.
    1276           0 :       value = _value.copyWith(selection: value.selection);
    1277             :     }
    1278           2 :     _lastKnownRemoteTextEditingValue = value;
    1279             : 
    1280           4 :     if (value == _value) {
    1281             :       // This is possible, for example, when the numeric keyboard is input,
    1282             :       // the engine will notify twice for the same value.
    1283             :       // Track at https://github.com/flutter/flutter/issues/65811
    1284             :       return;
    1285             :     }
    1286             : 
    1287          12 :     if (value.text == _value.text && value.composing == _value.composing) {
    1288             :       // `selection` is the only change.
    1289           2 :       _handleSelectionChanged(value.selection, SelectionChangedCause.keyboard);
    1290             :     } else {
    1291           2 :       hideToolbar();
    1292             : 
    1293           2 :       if (_hasInputConnection) {
    1294          11 :         if (widget.obscureText && value.text.length == _value.text.length + 1) {
    1295           1 :           _obscureShowCharTicksPending = _kObscureShowLatestCharCursorTicks;
    1296           4 :           _obscureLatestCharIndex = _value.selection.baseOffset;
    1297             :         }
    1298             :       }
    1299             : 
    1300           2 :       _formatAndSetValue(value, SelectionChangedCause.keyboard);
    1301             :     }
    1302             : 
    1303             :     // Wherever the value is changed by the user, schedule a showCaretOnScreen
    1304             :     // to make sure the user can see the changes they just made. Programmatical
    1305             :     // changes to `textEditingValue` do not trigger the behavior even if the
    1306             :     // text field is focused.
    1307           2 :     _scheduleShowCaretOnScreen();
    1308           2 :     if (_hasInputConnection) {
    1309             :       // To keep the cursor from blinking while typing, we want to restart the
    1310             :       // cursor timer every time a new character is typed.
    1311           2 :       _stopCursorTimer(resetCharTicks: false);
    1312           2 :       _startCursorTimer();
    1313             :     }
    1314             :   }
    1315             : 
    1316           1 :   @override
    1317             :   void performAction(TextInputAction action) {
    1318             :     switch (action) {
    1319           1 :       case TextInputAction.newline:
    1320             :         // If this is a multiline EditableText, do nothing for a "newline"
    1321             :         // action; The newline is already inserted. Otherwise, finalize
    1322             :         // editing.
    1323           2 :         if (!_isMultiline) _finalizeEditing(action, shouldUnfocus: true);
    1324             :         break;
    1325           1 :       case TextInputAction.done:
    1326           1 :       case TextInputAction.go:
    1327           1 :       case TextInputAction.next:
    1328           1 :       case TextInputAction.previous:
    1329           1 :       case TextInputAction.search:
    1330           1 :       case TextInputAction.send:
    1331           1 :         _finalizeEditing(action, shouldUnfocus: true);
    1332             :         break;
    1333           1 :       case TextInputAction.continueAction:
    1334           1 :       case TextInputAction.emergencyCall:
    1335           1 :       case TextInputAction.join:
    1336           1 :       case TextInputAction.none:
    1337           1 :       case TextInputAction.route:
    1338           1 :       case TextInputAction.unspecified:
    1339             :         // Finalize editing, but don't give up focus because this keyboard
    1340             :         // action does not imply the user is done inputting information.
    1341           1 :         _finalizeEditing(action, shouldUnfocus: false);
    1342             :         break;
    1343             :     }
    1344             :   }
    1345             : 
    1346           0 :   @override
    1347             :   void performPrivateCommand(String action, Map<String, dynamic> data) {
    1348           0 :     widget.onAppPrivateCommand!(action, data);
    1349             :   }
    1350             : 
    1351           0 :   @override
    1352             :   void updateFloatingCursor(RawFloatingCursorPoint point) {
    1353             :     // unimplemented
    1354             :   }
    1355             :   
    1356           1 :   @pragma('vm:notify-debugger-on-exception')
    1357             :   void _finalizeEditing(TextInputAction action, {required bool shouldUnfocus}) {
    1358             :     // Take any actions necessary now that the user has completed editing.
    1359           2 :     if (widget.onEditingComplete != null) {
    1360             :       try {
    1361           3 :         widget.onEditingComplete!();
    1362             :       } catch (exception, stack) {
    1363           2 :         FlutterError.reportError(FlutterErrorDetails(
    1364             :           exception: exception,
    1365             :           stack: stack,
    1366             :           library: 'widgets',
    1367             :           context:
    1368           2 :               ErrorDescription('while calling onEditingComplete for $action'),
    1369             :         ));
    1370             :       }
    1371             :     } else {
    1372             :       // Default behavior if the developer did not provide an
    1373             :       // onEditingComplete callback: Finalize editing and remove focus, or move
    1374             :       // it to the next/previous field, depending on the action.
    1375           3 :       widget.controller.clearComposing();
    1376             :       if (shouldUnfocus) {
    1377             :         switch (action) {
    1378           1 :           case TextInputAction.none:
    1379           1 :           case TextInputAction.unspecified:
    1380           1 :           case TextInputAction.done:
    1381           1 :           case TextInputAction.go:
    1382           1 :           case TextInputAction.search:
    1383           1 :           case TextInputAction.send:
    1384           1 :           case TextInputAction.continueAction:
    1385           1 :           case TextInputAction.join:
    1386           1 :           case TextInputAction.route:
    1387           1 :           case TextInputAction.emergencyCall:
    1388           1 :           case TextInputAction.newline:
    1389           3 :             widget.focusNode.unfocus();
    1390             :             break;
    1391           1 :           case TextInputAction.next:
    1392           3 :             widget.focusNode.nextFocus();
    1393             :             break;
    1394           1 :           case TextInputAction.previous:
    1395           3 :             widget.focusNode.previousFocus();
    1396             :             break;
    1397             :         }
    1398             :       }
    1399             :     }
    1400             : 
    1401             :     // Invoke optional callback with the user's submitted content.
    1402             :     try {
    1403           5 :       widget.onSubmitted?.call(_value.text);
    1404             :     } catch (exception, stack) {
    1405           2 :       FlutterError.reportError(FlutterErrorDetails(
    1406             :         exception: exception,
    1407             :         stack: stack,
    1408             :         library: 'widgets',
    1409           2 :         context: ErrorDescription('while calling onSubmitted for $action'),
    1410             :       ));
    1411             :     }
    1412             :   }
    1413             : 
    1414             :   int _batchEditDepth = 0;
    1415             : 
    1416             :   /// Begins a new batch edit, within which new updates made to the text editing
    1417             :   /// value will not be sent to the platform text input plugin.
    1418             :   ///
    1419             :   /// Batch edits nest. When the outermost batch edit finishes, [endBatchEdit]
    1420             :   /// will attempt to send [currentTextEditingValue] to the text input plugin if
    1421             :   /// it detected a change.
    1422           2 :   void beginBatchEdit() {
    1423           4 :     _batchEditDepth += 1;
    1424             :   }
    1425             : 
    1426             :   /// Ends the current batch edit started by the last call to [beginBatchEdit],
    1427             :   /// and send [currentTextEditingValue] to the text input plugin if needed.
    1428             :   ///
    1429             :   /// Throws an error in debug mode if this [EditableText] is not in a batch
    1430             :   /// edit.
    1431           2 :   void endBatchEdit() {
    1432           4 :     _batchEditDepth -= 1;
    1433             :     assert(
    1434           4 :       _batchEditDepth >= 0,
    1435             :       'Unbalanced call to endBatchEdit: beginBatchEdit must be called first.',
    1436             :     );
    1437           2 :     _updateRemoteEditingValueIfNeeded();
    1438             :   }
    1439             : 
    1440           2 :   void _updateRemoteEditingValueIfNeeded() {
    1441           6 :     if (_batchEditDepth > 0 || !_hasInputConnection) return;
    1442           2 :     final localValue = _value;
    1443           4 :     if (localValue == _lastKnownRemoteTextEditingValue) return;
    1444           4 :     _textInputConnection!.setEditingState(localValue);
    1445           2 :     _lastKnownRemoteTextEditingValue = localValue;
    1446             :   }
    1447             : 
    1448           8 :   TextEditingValue get _value => widget.controller.value;
    1449           2 :   set _value(TextEditingValue value) {
    1450           6 :     widget.controller.value = value;
    1451             :   }
    1452             : 
    1453           8 :   bool get _hasFocus => widget.focusNode.hasFocus;
    1454           8 :   bool get _isMultiline => widget.maxLines != 1;
    1455             : 
    1456             :   // Finds the closest scroll offset to the current scroll offset that fully
    1457             :   // reveals the given caret rect. If the given rect's main axis extent is too
    1458             :   // large to be fully revealed in `renderEditable`, it will be centered along
    1459             :   // the main axis.
    1460             :   //
    1461             :   // If this is a multiline MongolEditableText (which means the Editable can only
    1462             :   // scroll horizontally), the given rect's width will first be extended to match
    1463             :   // `renderEditable.preferredLineWidth`, before the target scroll offset is
    1464             :   // calculated.
    1465           2 :   RevealedOffset _getOffsetToRevealCaret(Rect rect) {
    1466           6 :     if (!_scrollController!.position.allowImplicitScrolling) {
    1467           3 :       return RevealedOffset(offset: _scrollController!.offset, rect: rect);
    1468             :     }
    1469             : 
    1470           4 :     final editableSize = renderEditable.size;
    1471             :     final double additionalOffset;
    1472             :     final Offset unitOffset;
    1473             : 
    1474           2 :     if (!_isMultiline) {
    1475             :       // singleline
    1476           6 :       additionalOffset = rect.height >= editableSize.height
    1477             :           // Center `rect` if it's oversized.
    1478           5 :           ? editableSize.height / 2 - rect.center.dy
    1479             :           // Valid additional offsets range from (rect.bottom - size.height)
    1480             :           // to (rect.top). Pick the closest one if out of range.
    1481          10 :           : 0.0.clamp(rect.bottom - editableSize.height, rect.top);
    1482             :       unitOffset = const Offset(0, 1);
    1483             :     } else {
    1484             :       // multiline
    1485             :       // The caret is horizontally centered within the line. Expand the caret's
    1486             :       // height so that it spans the line because we're going to ensure that the
    1487             :       // entire expanded caret is scrolled into view.
    1488           2 :       final expandedRect = Rect.fromCenter(
    1489           2 :         center: rect.center,
    1490           2 :         height: rect.height,
    1491           8 :         width: math.max(rect.width, renderEditable.preferredLineWidth),
    1492             :       );
    1493             : 
    1494           6 :       additionalOffset = expandedRect.width >= editableSize.width
    1495           5 :           ? editableSize.width / 2 - expandedRect.center.dx
    1496           2 :           : 0.0.clamp(
    1497           8 :               expandedRect.right - editableSize.width, expandedRect.left);
    1498             :       unitOffset = const Offset(1, 0);
    1499             :     }
    1500             : 
    1501             :     // No overscrolling when encountering tall fonts/scripts that extend past
    1502             :     // the ascent.
    1503           8 :     final targetOffset = (additionalOffset + _scrollController!.offset).clamp(
    1504           6 :       _scrollController!.position.minScrollExtent,
    1505           6 :       _scrollController!.position.maxScrollExtent,
    1506             :     );
    1507             : 
    1508           6 :     final offsetDelta = _scrollController!.offset - targetOffset;
    1509           2 :     return RevealedOffset(
    1510           4 :         rect: rect.shift(unitOffset * offsetDelta), offset: targetOffset);
    1511             :   }
    1512             : 
    1513           6 :   bool get _hasInputConnection => _textInputConnection?.attached ?? false;
    1514           7 :   bool get _needsAutofill => widget.autofillHints?.isNotEmpty ?? false;
    1515           2 :   bool get _shouldBeInAutofillContext =>
    1516           2 :       _needsAutofill && currentAutofillScope != null;
    1517             : 
    1518           2 :   void _openInputConnection() {
    1519           2 :     if (!_shouldCreateInputConnection) {
    1520             :       return;
    1521             :     }
    1522           2 :     if (!_hasInputConnection) {
    1523           2 :       final localValue = _value;
    1524             : 
    1525             :       // When _needsAutofill == true && currentAutofillScope == null, autofill
    1526             :       // is allowed but saving the user input from the text field is
    1527             :       // discouraged.
    1528             :       //
    1529             :       // In case the autofillScope changes from a non-null value to null, or
    1530             :       // _needsAutofill changes to false from true, the platform needs to be
    1531             :       // notified to exclude this field from the autofill context. So we need to
    1532             :       // provide the autofillId.
    1533           5 :       _textInputConnection = _needsAutofill && currentAutofillScope != null
    1534           0 :           ? currentAutofillScope!.attach(this, textInputConfiguration)
    1535           2 :           : TextInput.attach(
    1536             :               this,
    1537           2 :               _createTextInputConfiguration(
    1538           4 :                   _isInAutofillContext || _needsAutofill));
    1539           4 :       _textInputConnection!.show();
    1540           2 :       _updateSizeAndTransform();
    1541           2 :       _updateComposingRectIfNeeded();
    1542           2 :       if (_needsAutofill) {
    1543             :         // Request autofill AFTER the size and the transform have been sent to
    1544             :         // the platform text input plugin.
    1545           2 :         _textInputConnection!.requestAutofill();
    1546             :       }
    1547             : 
    1548           4 :       final style = widget.style;
    1549           2 :       _textInputConnection!
    1550           2 :         ..setStyle(
    1551           2 :           fontFamily: style.fontFamily,
    1552           2 :           fontSize: style.fontSize,
    1553           2 :           fontWeight: style.fontWeight,
    1554             :           textDirection: TextDirection.ltr,
    1555           6 :           textAlign: _rotatedTextAlign(widget.textAlign),
    1556             :         )
    1557           2 :         ..setEditingState(localValue);
    1558             :     } else {
    1559           4 :       _textInputConnection!.show();
    1560             :     }
    1561             :   }
    1562             : 
    1563           2 :   void _closeInputConnectionIfNeeded() {
    1564           2 :     if (_hasInputConnection) {
    1565           4 :       _textInputConnection!.close();
    1566           2 :       _textInputConnection = null;
    1567           2 :       _lastKnownRemoteTextEditingValue = null;
    1568             :     }
    1569             :   }
    1570             : 
    1571           2 :   void _openOrCloseInputConnectionIfNeeded() {
    1572           8 :     if (_hasFocus && widget.focusNode.consumeKeyboardToken()) {
    1573           2 :       _openInputConnection();
    1574           2 :     } else if (!_hasFocus) {
    1575           2 :       _closeInputConnectionIfNeeded();
    1576           6 :       widget.controller.clearComposing();
    1577             :     }
    1578             :   }
    1579             : 
    1580           1 :   @override
    1581             :   void connectionClosed() {
    1582           1 :     if (_hasInputConnection) {
    1583           2 :       _textInputConnection!.connectionClosedReceived();
    1584           1 :       _textInputConnection = null;
    1585           1 :       _lastKnownRemoteTextEditingValue = null;
    1586           1 :       _finalizeEditing(TextInputAction.done, shouldUnfocus: true);
    1587             :     }
    1588             :   }
    1589             : 
    1590             :   /// Express interest in interacting with the keyboard.
    1591             :   ///
    1592             :   /// If this control is already attached to the keyboard, this function will
    1593             :   /// request that the keyboard become visible. Otherwise, this function will
    1594             :   /// ask the focus system that it become focused. If successful in acquiring
    1595             :   /// focus, the control will then attach to the keyboard and request that the
    1596             :   /// keyboard become visible.
    1597           2 :   void requestKeyboard() {
    1598           2 :     if (_hasFocus) {
    1599           2 :       _openInputConnection();
    1600             :     } else {
    1601           6 :       widget.focusNode.requestFocus();
    1602             :     }
    1603             :   }
    1604             : 
    1605           2 :   void _updateOrDisposeSelectionOverlayIfNeeded() {
    1606           2 :     if (_selectionOverlay != null) {
    1607           2 :       if (_hasFocus) {
    1608           6 :         _selectionOverlay!.update(_value);
    1609             :       } else {
    1610           4 :         _selectionOverlay!.dispose();
    1611           2 :         _selectionOverlay = null;
    1612             :       }
    1613             :     }
    1614             :   }
    1615             :   
    1616           2 :   @pragma('vm:notify-debugger-on-exception')
    1617             :   void _handleSelectionChanged(
    1618             :       TextSelection selection, SelectionChangedCause? cause) {
    1619             :     // We return early if the selection is not valid. This can happen when the
    1620             :     // text of [MongolEditableText] is updated at the same time as the selection is
    1621             :     // changed by a gesture event.
    1622           6 :     if (!widget.controller.isSelectionWithinTextBounds(selection)) return;
    1623             : 
    1624           6 :     widget.controller.selection = selection;
    1625             : 
    1626             :     // This will show the keyboard for all selection changes on the
    1627             :     // editable widget, not just changes triggered by user gestures.
    1628           2 :     requestKeyboard();
    1629           4 :     if (widget.selectionControls == null) {
    1630           2 :       _selectionOverlay?.hide();
    1631           2 :       _selectionOverlay = null;
    1632             :     } else {
    1633           2 :       if (_selectionOverlay == null) {
    1634           4 :         _selectionOverlay = MongolTextSelectionOverlay(
    1635           2 :           clipboardStatus: _clipboardStatus,
    1636           2 :           context: context,
    1637           2 :           value: _value,
    1638           2 :           debugRequiredFor: widget,
    1639           2 :           toolbarLayerLink: _toolbarLayerLink,
    1640           2 :           startHandleLayerLink: _startHandleLayerLink,
    1641           2 :           endHandleLayerLink: _endHandleLayerLink,
    1642           2 :           renderObject: renderEditable,
    1643           4 :           selectionControls: widget.selectionControls,
    1644             :           selectionDelegate: this,
    1645           4 :           dragStartBehavior: widget.dragStartBehavior,
    1646           4 :           onSelectionHandleTapped: widget.onSelectionHandleTapped,
    1647             :         );
    1648             :       } else {
    1649           6 :         _selectionOverlay!.update(_value);
    1650             :       }
    1651           8 :       _selectionOverlay!.handlesVisible = widget.showSelectionHandles;
    1652           4 :       _selectionOverlay!.showHandles();
    1653             :     }
    1654             :     // TODO(chunhtai): we should make sure selection actually changed before
    1655             :     // we call the onSelectionChanged.
    1656             :     // https://github.com/flutter/flutter/issues/76349.
    1657             :     try {
    1658           6 :       widget.onSelectionChanged?.call(selection, cause);
    1659             :     } catch (exception, stack) {
    1660           2 :       FlutterError.reportError(FlutterErrorDetails(
    1661             :         exception: exception,
    1662             :         stack: stack,
    1663             :         library: 'widgets',
    1664             :         context:
    1665           2 :             ErrorDescription('while calling onSelectionChanged for $cause'),
    1666             :       ));
    1667             :     }
    1668             : 
    1669             :     // To keep the cursor from blinking while it moves, restart the timer here.
    1670           2 :     if (_cursorTimer != null) {
    1671           2 :       _stopCursorTimer(resetCharTicks: false);
    1672           2 :       _startCursorTimer();
    1673             :     }
    1674             :   }
    1675             : 
    1676             :   Rect? _currentCaretRect;
    1677           2 :   void _handleCaretChanged(Rect caretRect) {
    1678           2 :     _currentCaretRect = caretRect;
    1679             :   }
    1680             : 
    1681             :   // Animation configuration for scrolling the caret back on screen.
    1682             :   static const Duration _caretAnimationDuration = Duration(milliseconds: 100);
    1683             :   static const Curve _caretAnimationCurve = Curves.fastOutSlowIn;
    1684             : 
    1685             :   bool _showCaretOnScreenScheduled = false;
    1686             : 
    1687           2 :   void _scheduleShowCaretOnScreen() {
    1688           2 :     if (_showCaretOnScreenScheduled) {
    1689             :       return;
    1690             :     }
    1691           2 :     _showCaretOnScreenScheduled = true;
    1692           6 :     SchedulerBinding.instance!.addPostFrameCallback((Duration _) {
    1693           2 :       _showCaretOnScreenScheduled = false;
    1694           6 :       if (_currentCaretRect == null || !_scrollController!.hasClients) {
    1695             :         return;
    1696             :       }
    1697             : 
    1698           4 :       final lineWidth = renderEditable.preferredLineWidth;
    1699             : 
    1700             :       // Enlarge the target rect by scrollPadding to ensure that caret is not
    1701             :       // positioned directly at the edge after scrolling.
    1702           6 :       var rightSpacing = widget.scrollPadding.right;
    1703           4 :       if (_selectionOverlay?.selectionControls != null) {
    1704           4 :         final handleWidth = _selectionOverlay!.selectionControls!
    1705           2 :             .getHandleSize(lineWidth)
    1706           2 :             .width;
    1707           2 :         final interactiveHandleWidth = math.max(
    1708             :           handleWidth,
    1709             :           kMinInteractiveDimension,
    1710             :         );
    1711           6 :         final anchor = _selectionOverlay!.selectionControls!.getHandleAnchor(
    1712             :           TextSelectionHandleType.collapsed,
    1713             :           lineWidth,
    1714             :         );
    1715           6 :         final handleCenter = handleWidth / 2 - anchor.dx;
    1716           2 :         rightSpacing = math.max(
    1717           4 :           handleCenter + interactiveHandleWidth / 2,
    1718             :           rightSpacing,
    1719             :         );
    1720             :       }
    1721             : 
    1722           6 :       final caretPadding = widget.scrollPadding.copyWith(right: rightSpacing);
    1723             : 
    1724           4 :       final targetOffset = _getOffsetToRevealCaret(_currentCaretRect!);
    1725             : 
    1726           4 :       _scrollController!.animateTo(
    1727           2 :         targetOffset.offset,
    1728             :         duration: _caretAnimationDuration,
    1729             :         curve: _caretAnimationCurve,
    1730             :       );
    1731             : 
    1732           4 :       renderEditable.showOnScreen(
    1733           4 :         rect: caretPadding.inflateRect(targetOffset.rect),
    1734             :         duration: _caretAnimationDuration,
    1735             :         curve: _caretAnimationCurve,
    1736             :       );
    1737             :     });
    1738             :   }
    1739             : 
    1740             :   late double _lastRightViewInset;
    1741             : 
    1742           0 :   @override
    1743             :   void didChangeMetrics() {
    1744           0 :     if (_lastRightViewInset <
    1745           0 :         WidgetsBinding.instance!.window.viewInsets.right) {
    1746           0 :       _scheduleShowCaretOnScreen();
    1747             :     }
    1748           0 :     _lastRightViewInset = WidgetsBinding.instance!.window.viewInsets.right;
    1749             :   }
    1750             : 
    1751           2 :   @pragma('vm:notify-debugger-on-exception')
    1752             :   void _formatAndSetValue(TextEditingValue value, SelectionChangedCause? cause,
    1753             :       {bool userInteraction = false}) {
    1754             :     // Only apply input formatters if the text has changed (including uncommited
    1755             :     // text in the composing region), or when the user committed the composing
    1756             :     // text.
    1757             :     // Gboard is very persistent in restoring the composing region. Applying
    1758             :     // input formatters on composing-region-only changes (except clearing the
    1759             :     // current composing region) is very infinite-loop-prone: the formatters
    1760             :     // will keep trying to modify the composing region while Gboard will keep
    1761             :     // trying to restore the original composing region.
    1762           8 :     final textChanged = _value.text != value.text ||
    1763           6 :         (!_value.composing.isCollapsed && value.composing.isCollapsed);
    1764           8 :     final selectionChanged = _value.selection != value.selection;
    1765             : 
    1766             :     if (textChanged) {
    1767           6 :       value = widget.inputFormatters?.fold<TextEditingValue>(
    1768             :             value,
    1769           2 :             (TextEditingValue newValue, TextInputFormatter formatter) =>
    1770           4 :                 formatter.formatEditUpdate(_value, newValue),
    1771             :           ) ??
    1772             :           value;
    1773             :     }
    1774             : 
    1775             :     // Put all optional user callback invocations in a batch edit to prevent
    1776             :     // sending multiple `TextInput.updateEditingValue` messages.
    1777           2 :     beginBatchEdit();
    1778           2 :     _value = value;
    1779             :     // Changes made by the keyboard can sometimes be "out of band" for listening
    1780             :     // components, so always send those events, even if we didn't think it
    1781             :     // changed. Also, the user long pressing should always send a selection change
    1782             :     // as well.
    1783             :     if (selectionChanged ||
    1784             :         (userInteraction &&
    1785           2 :             (cause == SelectionChangedCause.longPress ||
    1786           2 :                 cause == SelectionChangedCause.keyboard))) {
    1787           4 :       _handleSelectionChanged(value.selection, cause);
    1788             :     }
    1789             :     if (textChanged) {
    1790             :       try {
    1791           8 :         widget.onChanged?.call(value.text);
    1792             :       } catch (exception, stack) {
    1793           2 :         FlutterError.reportError(FlutterErrorDetails(
    1794             :           exception: exception,
    1795             :           stack: stack,
    1796             :           library: 'widgets',
    1797           1 :           context: ErrorDescription('while calling onChanged'),
    1798             :         ));
    1799             :       }
    1800             :     }
    1801             : 
    1802           2 :     endBatchEdit();
    1803             :   }
    1804             : 
    1805           2 :   void _onCursorColorTick() {
    1806           4 :     renderEditable.cursorColor =
    1807          10 :         widget.cursorColor.withOpacity(_cursorBlinkOpacityController.value);
    1808           4 :     _cursorVisibilityNotifier.value =
    1809          10 :         widget.showCursor && _cursorBlinkOpacityController.value > 0;
    1810             :   }
    1811             : 
    1812             :   /// Whether the blinking cursor is actually visible at this precise moment
    1813             :   /// (it's hidden half the time, since it blinks).
    1814           1 :   @visibleForTesting
    1815           3 :   bool get cursorCurrentlyVisible => _cursorBlinkOpacityController.value > 0;
    1816             : 
    1817             :   /// The cursor blink interval (the amount of time the cursor is in the "on"
    1818             :   /// state or the "off" state). A complete cursor blink period is twice this
    1819             :   /// value (half on, half off).
    1820           1 :   @visibleForTesting
    1821             :   Duration get cursorBlinkInterval => _kCursorBlinkHalfPeriod;
    1822             : 
    1823             :   /// The current status of the text selection handles.
    1824           1 :   @visibleForTesting
    1825           1 :   MongolTextSelectionOverlay? get selectionOverlay => _selectionOverlay;
    1826             : 
    1827             :   int _obscureShowCharTicksPending = 0;
    1828             :   int? _obscureLatestCharIndex;
    1829             : 
    1830           2 :   void _cursorTick(Timer timer) {
    1831           4 :     _targetCursorVisibility = !_targetCursorVisibility;
    1832           2 :     final targetOpacity = _targetCursorVisibility ? 1.0 : 0.0;
    1833           4 :     if (widget.cursorOpacityAnimates) {
    1834             :       // If we want to show the cursor, we will animate the opacity to the value
    1835             :       // of 1.0, and likewise if we want to make it disappear, to 0.0. An easing
    1836             :       // curve is used for the animation to mimic the aesthetics of the native
    1837             :       // iOS cursor.
    1838             :       //
    1839             :       // These values and curves have been obtained through eyeballing, so are
    1840             :       // likely not exactly the same as the values for native iOS.
    1841           2 :       _cursorBlinkOpacityController.animateTo(targetOpacity,
    1842             :           curve: Curves.easeOut);
    1843             :     } else {
    1844           4 :       _cursorBlinkOpacityController.value = targetOpacity;
    1845             :     }
    1846             : 
    1847           4 :     if (_obscureShowCharTicksPending > 0) {
    1848           2 :       setState(() {
    1849           2 :         _obscureShowCharTicksPending--;
    1850             :       });
    1851             :     }
    1852             :   }
    1853             : 
    1854           1 :   void _cursorWaitForStart(Timer timer) {
    1855           1 :     assert(_kCursorBlinkHalfPeriod > _fadeDuration);
    1856           2 :     _cursorTimer?.cancel();
    1857           3 :     _cursorTimer = Timer.periodic(_kCursorBlinkHalfPeriod, _cursorTick);
    1858             :   }
    1859             : 
    1860           2 :   void _startCursorTimer() {
    1861           2 :     _targetCursorVisibility = true;
    1862           4 :     _cursorBlinkOpacityController.value = 1.0;
    1863             :     if (MongolEditableText.debugDeterministicCursor) return;
    1864           4 :     if (widget.cursorOpacityAnimates) {
    1865           1 :       _cursorTimer =
    1866           2 :           Timer.periodic(_kCursorBlinkWaitForStart, _cursorWaitForStart);
    1867             :     } else {
    1868           6 :       _cursorTimer = Timer.periodic(_kCursorBlinkHalfPeriod, _cursorTick);
    1869             :     }
    1870             :   }
    1871             : 
    1872           2 :   void _stopCursorTimer({bool resetCharTicks = true}) {
    1873           4 :     _cursorTimer?.cancel();
    1874           2 :     _cursorTimer = null;
    1875           2 :     _targetCursorVisibility = false;
    1876           4 :     _cursorBlinkOpacityController.value = 0.0;
    1877             :     if (MongolEditableText.debugDeterministicCursor) return;
    1878           2 :     if (resetCharTicks) _obscureShowCharTicksPending = 0;
    1879           4 :     if (widget.cursorOpacityAnimates) {
    1880           2 :       _cursorBlinkOpacityController.stop();
    1881           2 :       _cursorBlinkOpacityController.value = 0.0;
    1882             :     }
    1883             :   }
    1884             : 
    1885           2 :   void _startOrStopCursorTimerIfNeeded() {
    1886          10 :     if (_cursorTimer == null && _hasFocus && _value.selection.isCollapsed) {
    1887           2 :       _startCursorTimer();
    1888           2 :     } else if (_cursorTimer != null &&
    1889           8 :         (!_hasFocus || !_value.selection.isCollapsed)) {
    1890           2 :       _stopCursorTimer();
    1891             :     }
    1892             :   }
    1893             : 
    1894           2 :   void _didChangeTextEditingValue() {
    1895           2 :     _updateRemoteEditingValueIfNeeded();
    1896           2 :     _startOrStopCursorTimerIfNeeded();
    1897           2 :     _updateOrDisposeSelectionOverlayIfNeeded();
    1898             :     // TODO(abarth): Teach RenderEditable about ValueNotifier<TextEditingValue>
    1899             :     // to avoid this setState().
    1900           4 :     setState(() {/* We use widget.controller.value in build(). */});
    1901             :   }
    1902             : 
    1903           2 :   void _handleFocusChanged() {
    1904           2 :     _openOrCloseInputConnectionIfNeeded();
    1905           2 :     _startOrStopCursorTimerIfNeeded();
    1906           2 :     _updateOrDisposeSelectionOverlayIfNeeded();
    1907           2 :     if (_hasFocus) {
    1908             :       // Listen for changing viewInsets, which indicates keyboard showing up.
    1909           4 :       WidgetsBinding.instance!.addObserver(this);
    1910          10 :       _lastRightViewInset = WidgetsBinding.instance!.window.viewInsets.right;
    1911           4 :       if (!widget.readOnly) {
    1912           2 :         _scheduleShowCaretOnScreen();
    1913             :       }
    1914           6 :       if (!_value.selection.isValid) {
    1915             :         // Place cursor at the end if the selection is invalid when we receive focus.
    1916           2 :         _handleSelectionChanged(
    1917           8 :             TextSelection.collapsed(offset: _value.text.length), null);
    1918             :       }
    1919             :     } else {
    1920           4 :       WidgetsBinding.instance!.removeObserver(this);
    1921             :       // Clear the selection and composition state if this widget lost focus.
    1922           8 :       _value = TextEditingValue(text: _value.text);
    1923             :     }
    1924           2 :     updateKeepAlive();
    1925             :   }
    1926             : 
    1927           2 :   void _updateSizeAndTransform() {
    1928           2 :     if (_hasInputConnection) {
    1929           4 :       final size = renderEditable.size;
    1930           4 :       final transform = renderEditable.getTransformTo(null);
    1931           4 :       _textInputConnection!.setEditableSizeAndTransform(size, transform);
    1932           2 :       SchedulerBinding.instance!
    1933           6 :           .addPostFrameCallback((Duration _) => _updateSizeAndTransform());
    1934             :     }
    1935             :   }
    1936             : 
    1937             :   // Sends the current composing rect to the iOS text input plugin via the text
    1938             :   // input channel. We need to keep sending the information even if no text is
    1939             :   // currently marked, as the information usually lags behind. The text input
    1940             :   // plugin needs to estimate the composing rect based on the latest caret rect,
    1941             :   // when the composing rect info didn't arrive in time.
    1942           2 :   void _updateComposingRectIfNeeded() {
    1943           4 :     final composingRange = _value.composing;
    1944           2 :     if (_hasInputConnection) {
    1945           2 :       assert(mounted);
    1946             :       var composingRect =
    1947           4 :           renderEditable.getRectForComposingRange(composingRange);
    1948             :       // Send the caret location instead if there's no marked text yet.
    1949             :       if (composingRect == null) {
    1950           2 :         assert(!composingRange.isValid || composingRange.isCollapsed);
    1951           2 :         final offset = composingRange.isValid ? composingRange.start : 0;
    1952             :         composingRect =
    1953           6 :             renderEditable.getLocalRectForCaret(TextPosition(offset: offset));
    1954             :       }
    1955           4 :       _textInputConnection!.setComposingRect(composingRect);
    1956           2 :       SchedulerBinding.instance!
    1957           6 :           .addPostFrameCallback((Duration _) => _updateComposingRectIfNeeded());
    1958             :     }
    1959             :   }
    1960             : 
    1961             :   /// The renderer for this widget's descendant.
    1962             :   ///
    1963             :   /// This property is typically used to notify the renderer of input gestures
    1964             :   /// when [MongolRenderEditable.ignorePointer] is true.
    1965           2 :   @override
    1966             :   MongolRenderEditable get renderEditable =>
    1967           6 :       _editableKey.currentContext!.findRenderObject()! as MongolRenderEditable;
    1968             : 
    1969           2 :   @override
    1970           2 :   TextEditingValue get textEditingValue => _value;
    1971             : 
    1972           8 :   double get _devicePixelRatio => MediaQuery.of(context).devicePixelRatio;
    1973             : 
    1974           2 :   @override
    1975             :   void userUpdateTextEditingValue(
    1976             :       TextEditingValue value, SelectionChangedCause? cause) {
    1977             :     // Compare the current TextEditingValue with the pre-format new
    1978             :     // TextEditingValue value, in case the formatter would reject the change.
    1979             :     final shouldShowCaret =
    1980          16 :         widget.readOnly ? _value.selection != value.selection : _value != value;
    1981             :     if (shouldShowCaret) {
    1982           2 :       _scheduleShowCaretOnScreen();
    1983             :     }
    1984           2 :     _formatAndSetValue(value, cause, userInteraction: true);
    1985             :   }
    1986             : 
    1987           1 :   @override
    1988             :   void bringIntoView(TextPosition position) {
    1989           2 :     final localRect = renderEditable.getLocalRectForCaret(position);
    1990           1 :     final targetOffset = _getOffsetToRevealCaret(localRect);
    1991             : 
    1992           3 :     _scrollController!.jumpTo(targetOffset.offset);
    1993           3 :     renderEditable.showOnScreen(rect: targetOffset.rect);
    1994             :   }
    1995             : 
    1996             :   /// Shows the selection toolbar at the location of the current cursor.
    1997             :   ///
    1998             :   /// Returns `false` if a toolbar couldn't be shown, such as when the toolbar
    1999             :   /// is already shown, or when no text selection currently exists.
    2000           2 :   bool showToolbar() {
    2001             :     // Web is using native dom elements to enable clipboard functionality of the
    2002             :     // toolbar: copy, paste, select, cut. It might also provide additional
    2003             :     // functionality depending on the browser (such as translate). Due to this
    2004             :     // we should not show a Flutter toolbar for the editable text elements.
    2005             :     if (kIsWeb) {
    2006             :       return false;
    2007             :     }
    2008             : 
    2009           6 :     if (_selectionOverlay == null || _selectionOverlay!.toolbarIsVisible) {
    2010             :       return false;
    2011             :     }
    2012             : 
    2013           4 :     _selectionOverlay!.showToolbar();
    2014             :     return true;
    2015             :   }
    2016             : 
    2017           2 :   @override
    2018             :   void hideToolbar([bool hideHandles = true]) {
    2019             :     if (hideHandles) {
    2020             :       // Hide the handles and the toolbar.
    2021           4 :       _selectionOverlay?.hide();
    2022             :     } else {
    2023             :       // Hide only the toolbar but not the handles.
    2024           0 :       _selectionOverlay?.hideToolbar();
    2025             :     }
    2026             :   }
    2027             : 
    2028             :   /// Toggles the visibility of the toolbar.
    2029           1 :   void toggleToolbar() {
    2030           1 :     assert(_selectionOverlay != null);
    2031           2 :     if (_selectionOverlay!.toolbarIsVisible) {
    2032           0 :       hideToolbar();
    2033             :     } else {
    2034           1 :       showToolbar();
    2035             :     }
    2036             :   }
    2037             : 
    2038           1 :   @override
    2039           2 :   String get autofillId => 'MongolEditableText-$hashCode';
    2040             : 
    2041           2 :   TextInputConfiguration _createTextInputConfiguration(
    2042             :       bool needsAutofillConfiguration) {
    2043           2 :     return TextInputConfiguration(
    2044           4 :       inputType: widget.keyboardType,
    2045           4 :       readOnly: widget.readOnly,
    2046           4 :       obscureText: widget.obscureText,
    2047           4 :       autocorrect: widget.autocorrect,
    2048           4 :       enableSuggestions: widget.enableSuggestions,
    2049           4 :       inputAction: widget.textInputAction ??
    2050           6 :           (widget.keyboardType == TextInputType.multiline
    2051             :               ? TextInputAction.newline
    2052             :               : TextInputAction.done),
    2053           4 :       keyboardAppearance: widget.keyboardAppearance,
    2054             :       autofillConfiguration: !needsAutofillConfiguration
    2055             :           ? null
    2056           1 :           : AutofillConfiguration(
    2057           1 :               uniqueIdentifier: autofillId,
    2058             :               autofillHints:
    2059           3 :                   widget.autofillHints?.toList(growable: false) ?? <String>[],
    2060           1 :               currentEditingValue: currentTextEditingValue,
    2061             :             ),
    2062             :     );
    2063             :   }
    2064             : 
    2065           0 :   @override
    2066             :   TextInputConfiguration get textInputConfiguration {
    2067           0 :     return _createTextInputConfiguration(_needsAutofill);
    2068             :   }
    2069             : 
    2070           0 :   @override
    2071             :   void showAutocorrectionPromptRect(int start, int end) {
    2072             :     // unimplemented
    2073             :   }
    2074             : 
    2075           2 :   VoidCallback? _semanticsOnCopy(TextSelectionControls? controls) {
    2076           4 :     return widget.selectionEnabled &&
    2077           2 :             copyEnabled &&
    2078           2 :             _hasFocus &&
    2079           4 :             controls?.canCopy(this) == true
    2080           0 :         ? () => controls!.handleCopy(this, _clipboardStatus)
    2081             :         : null;
    2082             :   }
    2083             : 
    2084           2 :   VoidCallback? _semanticsOnCut(TextSelectionControls? controls) {
    2085           4 :     return widget.selectionEnabled &&
    2086           2 :             cutEnabled &&
    2087           2 :             _hasFocus &&
    2088           4 :             controls?.canCut(this) == true
    2089           0 :         ? () => controls!.handleCut(this)
    2090             :         : null;
    2091             :   }
    2092             : 
    2093           2 :   VoidCallback? _semanticsOnPaste(TextSelectionControls? controls) {
    2094           4 :     return widget.selectionEnabled &&
    2095           2 :             pasteEnabled &&
    2096           2 :             _hasFocus &&
    2097           4 :             controls?.canPaste(this) == true &&
    2098           2 :             (_clipboardStatus == null ||
    2099           6 :                 _clipboardStatus!.value == ClipboardStatus.pasteable)
    2100           0 :         ? () => controls!.handlePaste(this)
    2101             :         : null;
    2102             :   }
    2103             : 
    2104           2 :   @override
    2105             :   Widget build(BuildContext context) {
    2106           2 :     assert(debugCheckHasMediaQuery(context));
    2107           4 :     _focusAttachment!.reparent();
    2108           2 :     super.build(context); // See AutomaticKeepAliveClientMixin.
    2109             : 
    2110           4 :     final controls = widget.selectionControls;
    2111           2 :     return MouseRegion(
    2112           4 :       cursor: widget.mouseCursor ?? SystemMouseCursors.text,
    2113           2 :       child: Scrollable(
    2114             :         excludeFromSemantics: true,
    2115           2 :         axisDirection: _isMultiline ? AxisDirection.right : AxisDirection.down,
    2116           2 :         controller: _scrollController,
    2117           4 :         physics: widget.scrollPhysics,
    2118           4 :         dragStartBehavior: widget.dragStartBehavior,
    2119           4 :         restorationId: widget.restorationId,
    2120           4 :         scrollBehavior: widget.scrollBehavior ??
    2121             :             // Remove scrollbars if only single line
    2122           2 :             (_isMultiline
    2123             :                 ? null
    2124           4 :                 : ScrollConfiguration.of(context).copyWith(scrollbars: false)),
    2125           2 :         viewportBuilder: (BuildContext context, ViewportOffset offset) {
    2126           2 :           return CompositedTransformTarget(
    2127           2 :             link: _toolbarLayerLink,
    2128           2 :             child: Semantics(
    2129           2 :               onCopy: _semanticsOnCopy(controls),
    2130           2 :               onCut: _semanticsOnCut(controls),
    2131           2 :               onPaste: _semanticsOnPaste(controls),
    2132             :               textDirection: TextDirection.ltr,
    2133           2 :               child: _MongolEditable(
    2134           2 :                 key: _editableKey,
    2135           2 :                 startHandleLayerLink: _startHandleLayerLink,
    2136           2 :                 endHandleLayerLink: _endHandleLayerLink,
    2137           2 :                 textSpan: buildTextSpan(),
    2138           2 :                 value: _value,
    2139           2 :                 cursorColor: _cursorColor,
    2140             :                 showCursor: MongolEditableText.debugDeterministicCursor
    2141           0 :                     ? ValueNotifier<bool>(widget.showCursor)
    2142           2 :                     : _cursorVisibilityNotifier,
    2143           4 :                 forceLine: widget.forceLine,
    2144           4 :                 readOnly: widget.readOnly,
    2145           2 :                 hasFocus: _hasFocus,
    2146           4 :                 maxLines: widget.maxLines,
    2147           4 :                 minLines: widget.minLines,
    2148           4 :                 expands: widget.expands,
    2149           4 :                 selectionColor: widget.selectionColor,
    2150           4 :                 textScaleFactor: widget.textScaleFactor ??
    2151           2 :                     MediaQuery.textScaleFactorOf(context),
    2152           4 :                 textAlign: widget.textAlign,
    2153           4 :                 obscuringCharacter: widget.obscuringCharacter,
    2154           4 :                 obscureText: widget.obscureText,
    2155           4 :                 autocorrect: widget.autocorrect,
    2156           4 :                 enableSuggestions: widget.enableSuggestions,
    2157             :                 offset: offset,
    2158           2 :                 onCaretChanged: _handleCaretChanged,
    2159           4 :                 rendererIgnoresPointer: widget.rendererIgnoresPointer,
    2160           4 :                 cursorWidth: widget.cursorWidth,
    2161           4 :                 cursorHeight: widget.cursorHeight,
    2162           4 :                 cursorRadius: widget.cursorRadius,
    2163           4 :                 cursorOffset: widget.cursorOffset ?? Offset.zero,
    2164           4 :                 enableInteractiveSelection: widget.enableInteractiveSelection,
    2165             :                 textSelectionDelegate: this,
    2166           2 :                 devicePixelRatio: _devicePixelRatio,
    2167           4 :                 clipBehavior: widget.clipBehavior,
    2168             :               ),
    2169             :             ),
    2170             :           );
    2171             :         },
    2172             :       ),
    2173             :     );
    2174             :   }
    2175             : 
    2176             :   /// Builds [TextSpan] from current editing value.
    2177             :   ///
    2178             :   /// By default makes text in composing range appear as underlined.
    2179             :   /// Descendants can override this method to customize appearance of text.
    2180           2 :   TextSpan buildTextSpan() {
    2181           4 :     if (widget.obscureText) {
    2182           4 :       var text = _value.text;
    2183           8 :       text = widget.obscuringCharacter * text.length;
    2184             :       // Reveal the latest character in an obscured field only on mobile.
    2185           4 :       if (defaultTargetPlatform == TargetPlatform.android ||
    2186           2 :           defaultTargetPlatform == TargetPlatform.iOS ||
    2187           2 :           defaultTargetPlatform == TargetPlatform.fuchsia) {
    2188             :         final o =
    2189           5 :             _obscureShowCharTicksPending > 0 ? _obscureLatestCharIndex : null;
    2190           3 :         if (o != null && o >= 0 && o < text.length) {
    2191           6 :           text = text.replaceRange(o, o + 1, _value.text.substring(o, o + 1));
    2192             :         }
    2193             :       }
    2194           6 :       return TextSpan(style: widget.style, text: text);
    2195             :     }
    2196             :     // Read only mode should not paint text composing.
    2197           6 :     return widget.controller.buildTextSpan(
    2198           2 :       context: context,
    2199           4 :       style: widget.style,
    2200           4 :       withComposing: !widget.readOnly,
    2201             :     );
    2202             :   }
    2203             : }
    2204             : 
    2205             : class _MongolEditable extends LeafRenderObjectWidget {
    2206           2 :   const _MongolEditable({
    2207             :     Key? key,
    2208             :     required this.textSpan,
    2209             :     required this.value,
    2210             :     required this.startHandleLayerLink,
    2211             :     required this.endHandleLayerLink,
    2212             :     this.cursorColor,
    2213             :     required this.showCursor,
    2214             :     required this.forceLine,
    2215             :     required this.readOnly,
    2216             :     required this.hasFocus,
    2217             :     required this.maxLines,
    2218             :     this.minLines,
    2219             :     required this.expands,
    2220             :     this.selectionColor,
    2221             :     required this.textScaleFactor,
    2222             :     required this.textAlign,
    2223             :     required this.obscuringCharacter,
    2224             :     required this.obscureText,
    2225             :     required this.autocorrect,
    2226             :     required this.enableSuggestions,
    2227             :     required this.offset,
    2228             :     this.onCaretChanged,
    2229             :     this.rendererIgnoresPointer = false,
    2230             :     this.cursorWidth,
    2231             :     required this.cursorHeight,
    2232             :     this.cursorRadius,
    2233             :     required this.cursorOffset,
    2234             :     this.enableInteractiveSelection = true,
    2235             :     required this.textSelectionDelegate,
    2236             :     required this.devicePixelRatio,
    2237             :     required this.clipBehavior,
    2238           2 :   }) : super(key: key);
    2239             : 
    2240             :   final TextSpan textSpan;
    2241             :   final TextEditingValue value;
    2242             :   final Color? cursorColor;
    2243             :   final LayerLink startHandleLayerLink;
    2244             :   final LayerLink endHandleLayerLink;
    2245             :   final ValueNotifier<bool> showCursor;
    2246             :   final bool forceLine;
    2247             :   final bool readOnly;
    2248             :   final bool hasFocus;
    2249             :   final int? maxLines;
    2250             :   final int? minLines;
    2251             :   final bool expands;
    2252             :   final Color? selectionColor;
    2253             :   final double textScaleFactor;
    2254             :   final MongolTextAlign textAlign;
    2255             :   final String obscuringCharacter;
    2256             :   final bool obscureText;
    2257             :   final bool autocorrect;
    2258             :   final bool enableSuggestions;
    2259             :   final ViewportOffset offset;
    2260             :   final CaretChangedHandler? onCaretChanged;
    2261             :   final bool rendererIgnoresPointer;
    2262             :   final double? cursorWidth;
    2263             :   final double cursorHeight;
    2264             :   final Radius? cursorRadius;
    2265             :   final Offset cursorOffset;
    2266             :   final bool enableInteractiveSelection;
    2267             :   final TextSelectionDelegate textSelectionDelegate;
    2268             :   final double devicePixelRatio;
    2269             :   final Clip clipBehavior;
    2270             : 
    2271           2 :   @override
    2272             :   MongolRenderEditable createRenderObject(BuildContext context) {
    2273           2 :     return MongolRenderEditable(
    2274           2 :       text: textSpan,
    2275           2 :       cursorColor: cursorColor,
    2276           2 :       startHandleLayerLink: startHandleLayerLink,
    2277           2 :       endHandleLayerLink: endHandleLayerLink,
    2278           2 :       showCursor: showCursor,
    2279           2 :       forceLine: forceLine,
    2280           2 :       readOnly: readOnly,
    2281           2 :       hasFocus: hasFocus,
    2282           2 :       maxLines: maxLines,
    2283           2 :       minLines: minLines,
    2284           2 :       expands: expands,
    2285           2 :       selectionColor: selectionColor,
    2286           2 :       textScaleFactor: textScaleFactor,
    2287           2 :       textAlign: textAlign,
    2288           4 :       selection: value.selection,
    2289           2 :       offset: offset,
    2290           2 :       onCaretChanged: onCaretChanged,
    2291           2 :       ignorePointer: rendererIgnoresPointer,
    2292           2 :       obscuringCharacter: obscuringCharacter,
    2293           2 :       obscureText: obscureText,
    2294           2 :       cursorWidth: cursorWidth,
    2295           2 :       cursorHeight: cursorHeight,
    2296           2 :       cursorRadius: cursorRadius,
    2297           2 :       cursorOffset: cursorOffset,
    2298           2 :       enableInteractiveSelection: enableInteractiveSelection,
    2299           2 :       textSelectionDelegate: textSelectionDelegate,
    2300           2 :       devicePixelRatio: devicePixelRatio,
    2301           2 :       clipBehavior: clipBehavior,
    2302             :     );
    2303             :   }
    2304             : 
    2305           2 :   @override
    2306             :   void updateRenderObject(
    2307             :       BuildContext context, MongolRenderEditable renderObject) {
    2308             :     renderObject
    2309           4 :       ..text = textSpan
    2310           4 :       ..cursorColor = cursorColor
    2311           4 :       ..startHandleLayerLink = startHandleLayerLink
    2312           4 :       ..endHandleLayerLink = endHandleLayerLink
    2313           4 :       ..showCursor = showCursor
    2314           4 :       ..forceLine = forceLine
    2315           4 :       ..readOnly = readOnly
    2316           4 :       ..hasFocus = hasFocus
    2317           4 :       ..maxLines = maxLines
    2318           4 :       ..minLines = minLines
    2319           4 :       ..expands = expands
    2320           4 :       ..selectionColor = selectionColor
    2321           4 :       ..textScaleFactor = textScaleFactor
    2322           4 :       ..textAlign = textAlign
    2323           6 :       ..selection = value.selection
    2324           4 :       ..offset = offset
    2325           4 :       ..onCaretChanged = onCaretChanged
    2326           4 :       ..ignorePointer = rendererIgnoresPointer
    2327           4 :       ..obscuringCharacter = obscuringCharacter
    2328           4 :       ..obscureText = obscureText
    2329           4 :       ..cursorWidth = cursorWidth
    2330           4 :       ..cursorHeight = cursorHeight
    2331           4 :       ..cursorRadius = cursorRadius
    2332           4 :       ..cursorOffset = cursorOffset
    2333           4 :       ..textSelectionDelegate = textSelectionDelegate
    2334           4 :       ..devicePixelRatio = devicePixelRatio
    2335           4 :       ..clipBehavior = clipBehavior;
    2336             :   }
    2337             : }

Generated by: LCOV version 1.15