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 : }
|