1 // Copyright 2014 The Flutter Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 /// @docImport 'package:flutter/cupertino.dart';
6 /// @docImport 'package:flutter/material.dart';
7 ///
8 /// @docImport 'app.dart';
9 /// @docImport 'context_menu_controller.dart';
10 /// @docImport 'form.dart';
11 /// @docImport 'restoration.dart';
12 /// @docImport 'restoration_properties.dart';
13 /// @docImport 'selectable_region.dart';
14 /// @docImport 'text_selection_toolbar_layout_delegate.dart';
15 library ;
16
17 import 'dart:async' ;
18 import 'dart:math' as math ;
19 import 'dart:ui' as ui hide TextStyle ;
20
21 import 'package:characters/characters.dart' show CharacterRange , StringCharacters ;
22 import 'package:flutter/foundation.dart' ;
23 import 'package:flutter/gestures.dart' ;
24 import 'package:flutter/rendering.dart' ;
25 import 'package:flutter/scheduler.dart' ;
26 import 'package:flutter/services.dart' ;
27
28 import '_web_browser_detection_io.dart' if (dart.library .js_util) '_web_browser_detection_web.dart' ;
29 import 'actions.dart' ;
30 import 'app_lifecycle_listener.dart' ;
31 import 'autofill.dart' ;
32 import 'automatic_keep_alive.dart' ;
33 import 'basic.dart' ;
34 import 'binding.dart' ;
35 import 'constants.dart' ;
36 import 'context_menu_button_item.dart' ;
37 import 'debug.dart' ;
38 import 'default_selection_style.dart' ;
39 import 'default_text_editing_shortcuts.dart' ;
40 import 'focus_manager.dart' ;
41 import 'focus_scope.dart' ;
42 import 'focus_traversal.dart' ;
43 import 'framework.dart' ;
44 import 'localizations.dart' ;
45 import 'magnifier.dart' ;
46 import 'media_query.dart' ;
47 import 'notification_listener.dart' ;
48 import 'scroll_configuration.dart' ;
49 import 'scroll_controller.dart' ;
50 import 'scroll_notification.dart' ;
51 import 'scroll_notification_observer.dart' ;
52 import 'scroll_physics.dart' ;
53 import 'scroll_position.dart' ;
54 import 'scrollable.dart' ;
55 import 'scrollable_helpers.dart' ;
56 import 'shortcuts.dart' ;
57 import 'size_changed_layout_notifier.dart' ;
58 import 'spell_check.dart' ;
59 import 'tap_region.dart' ;
60 import 'text.dart' ;
61 import 'text_editing_intents.dart' ;
62 import 'text_selection.dart' ;
63 import 'text_selection_toolbar_anchors.dart' ;
64 import 'ticker_provider.dart' ;
65 import 'undo_history.dart' ;
66 import 'view.dart' ;
67 import 'widget_span.dart' ;
68
69 export 'package:flutter/services.dart'
70 show
71 KeyboardInsertedContent ,
72 SelectionChangedCause ,
73 SmartDashesType ,
74 SmartQuotesType ,
75 TextEditingValue ,
76 TextInputType ,
77 TextSelection ;
78
79 // Examples can assume:
80 // late BuildContext context;
81 // late WidgetTester tester;
82
83 /// Signature for the callback that reports when the user changes the selection
84 /// (including the cursor location).
85 typedef SelectionChangedCallback =
86 void Function (TextSelection selection, SelectionChangedCause ? cause);
87
88 /// Signature for the callback that reports the app private command results.
89 typedef AppPrivateCommandCallback = void Function (String action, Map <String , dynamic > data);
90
91 /// Signature for a widget builder that builds a context menu for the given
92 /// [EditableTextState ].
93 ///
94 /// See also:
95 ///
96 /// * [SelectableRegionContextMenuBuilder], which performs the same role for
97 /// [SelectableRegion].
98 typedef EditableTextContextMenuBuilder =
99 Widget Function (BuildContext context, EditableTextState editableTextState);
100
101 // Signature for a function that determines the target location of the given
102 // [TextPosition] after applying the given [TextBoundary].
103 typedef _ApplyTextBoundary = TextPosition Function (TextPosition , bool , TextBoundary );
104
105 // The time it takes for the cursor to fade from fully opaque to fully
106 // transparent and vice versa. A full cursor blink, from transparent to opaque
107 // to transparent, is twice this duration.
108 const Duration _kCursorBlinkHalfPeriod = Duration (milliseconds : 500 );
109
110 // Number of cursor ticks during which the most recently entered character
111 // is shown in an obscured text field.
112 const int _kObscureShowLatestCharCursorTicks = 3 ;
113
114 /// The default mime types to be used when allowedMimeTypes is not provided.
115 ///
116 /// The default value supports inserting images of any supported format.
117 const List <String > kDefaultContentInsertionMimeTypes = <String >[
118 'image/png' ,
119 'image/bmp' ,
120 'image/jpg' ,
121 'image/tiff' ,
122 'image/gif' ,
123 'image/jpeg' ,
124 'image/webp' ,
125 ];
126
127 class _CompositionCallback extends SingleChildRenderObjectWidget {
128 const _CompositionCallback ({required this .compositeCallback , required this .enabled , super .child });
129 final CompositionCallback compositeCallback ;
130 final bool enabled ;
131
132 @override
133 RenderObject createRenderObject (BuildContext context ) {
134 return _RenderCompositionCallback (compositeCallback , enabled );
135 }
136
137 @override
138 void updateRenderObject (BuildContext context , _RenderCompositionCallback renderObject ) {
139 super .updateRenderObject (context , renderObject );
140 // _EditableTextState always uses the same callback.
141 assert (renderObject .compositeCallback == compositeCallback );
142 renderObject .enabled = enabled ;
143 }
144 }
145
146 class _RenderCompositionCallback extends RenderProxyBox {
147 _RenderCompositionCallback (this .compositeCallback , this ._enabled );
148
149 final CompositionCallback compositeCallback ;
150 VoidCallback ? _cancelCallback ;
151
152 bool get enabled => _enabled ;
153 bool _enabled = false ;
154 set enabled (bool newValue ) {
155 _enabled = newValue ;
156 if (!newValue ) {
157 _cancelCallback ?.call();
158 _cancelCallback = null ;
159 } else if (_cancelCallback == null ) {
160 markNeedsPaint ();
161 }
162 }
163
164 @override
165 void paint (PaintingContext context , ui.Offset offset ) {
166 if (enabled ) {
167 _cancelCallback ??= context .addCompositionCallback (compositeCallback );
168 }
169 super .paint (context , offset );
170 }
171 }
172
173 /// A controller for an editable text field.
174 ///
175 /// Whenever the user modifies a text field with an associated
176 /// [TextEditingController ], the text field updates [value ] and the controller
177 /// notifies its listeners. Listeners can then read the [text ] and [selection ]
178 /// properties to learn what the user has typed or how the selection has been
179 /// updated.
180 ///
181 /// Similarly, if you modify the [text ] or [selection ] properties, the text
182 /// field will be notified and will update itself appropriately.
183 ///
184 /// A [TextEditingController ] can also be used to provide an initial value for a
185 /// text field. If you build a text field with a controller that already has
186 /// [text ], the text field will use that text as its initial value.
187 ///
188 /// The [value ] (as well as [text ] and [selection ]) of this controller can be
189 /// updated from within a listener added to this controller. Be aware of
190 /// infinite loops since the listener will also be notified of the changes made
191 /// from within itself. Modifying the composing region from within a listener
192 /// can also have a bad interaction with some input methods. Gboard, for
193 /// example, will try to restore the composing region of the text if it was
194 /// modified programmatically, creating an infinite loop of communications
195 /// between the framework and the input method. Consider using
196 /// [TextInputFormatter ]s instead for as-you-type text modification.
197 ///
198 /// If both the [text ] and [selection ] properties need to be changed, set the
199 /// controller's [value ] instead. Setting [text ] will clear the selection
200 /// and composing range.
201 ///
202 /// Remember to [dispose ] of the [TextEditingController ] when it is no longer
203 /// needed. This will ensure we discard any resources used by the object.
204 ///
205 /// {@tool dartpad}
206 /// This example creates a [TextField] with a [TextEditingController ] whose
207 /// change listener forces the entered text to be lower case and keeps the
208 /// cursor at the end of the input.
209 ///
210 /// ** See code in examples/api/lib/widgets/editable_text/text_editing_controller.0.dart **
211 /// {@end-tool}
212 ///
213 /// See also:
214 ///
215 /// * [TextField], which is a Material Design text field that can be controlled
216 /// with a [TextEditingController ].
217 /// * [EditableText ], which is a raw region of editable text that can be
218 /// controlled with a [TextEditingController ].
219 /// * Learn how to use a [TextEditingController ] in one of our [cookbook recipes](https://docs.flutter.dev/cookbook/forms/text-field-changes#2-use-a-texteditingcontroller).
220 class TextEditingController extends ValueNotifier <TextEditingValue > {
221 /// Creates a controller for an editable text field, with no initial selection.
222 ///
223 /// This constructor treats a null [text ] argument as if it were the empty
224 /// string.
225 ///
226 /// The initial selection is `TextSelection.collapsed(offset: -1)`.
227 /// This indicates that there is no selection at all ([TextSelection .isValid ]
228 /// is false in this case). When a text field is built with a controller whose
229 /// selection is not valid, the text field will update the selection when it
230 /// is focused (the selection will be an empty selection positioned at the
231 /// end of the text).
232 ///
233 /// Consider using [TextEditingController .fromValue ] to initialize both the
234 /// text and the selection.
235 ///
236 /// {@tool dartpad}
237 /// This example creates a [TextField] with a [TextEditingController ] whose
238 /// initial selection is empty (collapsed) and positioned at the beginning
239 /// of the text (offset is 0).
240 ///
241 /// ** See code in examples/api/lib/widgets/editable_text/text_editing_controller.1.dart **
242 /// {@end-tool}
243 TextEditingController ({String ? text })
244 : super (text == null ? TextEditingValue .empty : TextEditingValue (text : text ));
245
246 /// Creates a controller for an editable text field from an initial [TextEditingValue ].
247 ///
248 /// This constructor treats a null [value ] argument as if it were
249 /// [TextEditingValue .empty ].
250 TextEditingController .fromValue (TextEditingValue ? value )
251 : assert (
252 value == null || !value .composing .isValid || value .isComposingRangeValid ,
253 'New TextEditingValue $value has an invalid non-empty composing range '
254 ' ${value .composing }. It is recommended to use a valid composing range, '
255 'even for readonly text fields.' ,
256 ),
257 super (value ?? TextEditingValue .empty );
258
259 /// The current string the user is editing.
260 String get text => value .text ;
261
262 /// Updates the current [text ] to the given `newText`, and removes existing
263 /// selection and composing range held by the controller.
264 ///
265 /// This setter is typically only used in tests, as it resets the cursor
266 /// position and the composing state. For production code, **consider using the
267 /// [value ] setter to update the [text ] value instead**, and specify a
268 /// reasonable selection range within the new [text ].
269 ///
270 /// Setting this notifies all the listeners of this [TextEditingController ]
271 /// that they need to update (it calls [notifyListeners ]). For this reason,
272 /// this value should only be set between frames, e.g. in response to user
273 /// actions, not during the build, layout, or paint phases. This property can
274 /// be set from a listener added to this [TextEditingController ].
275 set text (String newText ) {
276 value = value .copyWith (
277 text : newText ,
278 selection : const TextSelection .collapsed (offset : -1 ),
279 composing : TextRange .empty ,
280 );
281 }
282
283 @override
284 set value (TextEditingValue newValue ) {
285 assert (
286 !newValue .composing .isValid || newValue .isComposingRangeValid ,
287 'New TextEditingValue $newValue has an invalid non-empty composing range '
288 ' ${newValue .composing }. It is recommended to use a valid composing range, '
289 'even for readonly text fields.' ,
290 );
291 super .value = newValue ;
292 }
293
294 /// Builds [TextSpan ] from current editing value.
295 ///
296 /// By default makes text in composing range appear as underlined. Descendants
297 /// can override this method to customize appearance of text.
298 TextSpan buildTextSpan ({
299 required BuildContext context ,
300 TextStyle ? style ,
301 required bool withComposing ,
302 }) {
303 assert (!value .composing .isValid || !withComposing || value .isComposingRangeValid );
304 // If the composing range is out of range for the current text, ignore it to
305 // preserve the tree integrity, otherwise in release mode a RangeError will
306 // be thrown and this EditableText will be built with a broken subtree.
307 final bool composingRegionOutOfRange = !value .isComposingRangeValid || !withComposing ;
308
309 if (composingRegionOutOfRange ) {
310 return TextSpan (style : style , text : text );
311 }
312
313 final TextStyle composingStyle =
314 style ?.merge (const TextStyle (decoration : TextDecoration .underline )) ??
315 const TextStyle (decoration : TextDecoration .underline );
316 return TextSpan (
317 style : style ,
318 children : <TextSpan >[
319 TextSpan (text : value .composing .textBefore (value .text )),
320 TextSpan (style : composingStyle , text : value .composing .textInside (value .text )),
321 TextSpan (text : value .composing .textAfter (value .text )),
322 ],
323 );
324 }
325
326 /// The currently selected range within [text ].
327 ///
328 /// If the selection is collapsed, then this property gives the offset of the
329 /// cursor within the text.
330 TextSelection get selection => value .selection ;
331
332 /// Setting this will notify all the listeners of this [TextEditingController ]
333 /// that they need to update (it calls [notifyListeners ]). For this reason,
334 /// this value should only be set between frames, e.g. in response to user
335 /// actions, not during the build, layout, or paint phases.
336 ///
337 /// This property can be set from a listener added to this
338 /// [TextEditingController ]; however, one should not also set [text ]
339 /// in a separate statement. To change both the [text ] and the [selection ]
340 /// change the controller's [value ].
341 ///
342 /// If the new selection is outside the composing range, the composing range is
343 /// cleared.
344 set selection (TextSelection newSelection ) {
345 if (text .length < newSelection .end || text .length < newSelection .start ) {
346 throw FlutterError ('invalid text selection: $newSelection ' );
347 }
348 final TextRange newComposing = _isSelectionWithinComposingRange (newSelection )
349 ? value .composing
350 : TextRange .empty ;
351 value = value .copyWith (selection : newSelection , composing : newComposing );
352 }
353
354 /// Set the [value ] to empty.
355 ///
356 /// After calling this function, [text ] will be the empty string and the
357 /// selection will be collapsed at zero offset.
358 ///
359 /// Calling this will notify all the listeners of this [TextEditingController ]
360 /// that they need to update (it calls [notifyListeners ]). For this reason,
361 /// this method should only be called between frames, e.g. in response to user
362 /// actions, not during the build, layout, or paint phases.
363 void clear () {
364 value = const TextEditingValue (selection : TextSelection .collapsed (offset : 0 ));
365 }
366
367 /// Set the composing region to an empty range.
368 ///
369 /// The composing region is the range of text that is still being composed.
370 /// Calling this function indicates that the user is done composing that
371 /// region.
372 ///
373 /// Calling this will notify all the listeners of this [TextEditingController ]
374 /// that they need to update (it calls [notifyListeners ]). For this reason,
375 /// this method should only be called between frames, e.g. in response to user
376 /// actions, not during the build, layout, or paint phases.
377 void clearComposing () {
378 value = value .copyWith (composing : TextRange .empty );
379 }
380
381 /// Check that the [selection ] is inside of the composing range.
382 bool _isSelectionWithinComposingRange (TextSelection selection ) {
383 return selection .start >= value .composing .start && selection .end <= value .composing .end ;
384 }
385 }
386
387 /// Toolbar configuration for [EditableText ].
388 ///
389 /// Toolbar is a context menu that will show up when user right click or long
390 /// press the [EditableText ]. It includes several options: cut, copy, paste,
391 /// and select all.
392 ///
393 /// [EditableText ] and its derived widgets have their own default [ToolbarOptions ].
394 /// Create a custom [ToolbarOptions ] if you want explicit control over the toolbar
395 /// option.
396 @Deprecated (
397 'Use `contextMenuBuilder` instead. '
398 'This feature was deprecated after v3.3.0-0.5.pre.' ,
399 )
400 class ToolbarOptions {
401 /// Create a toolbar configuration with given options.
402 ///
403 /// All options default to false if they are not explicitly set.
404 @Deprecated (
405 'Use `contextMenuBuilder` instead. '
406 'This feature was deprecated after v3.3.0-0.5.pre.' ,
407 )
408 const ToolbarOptions ({
409 this .copy = false ,
410 this .cut = false ,
411 this .paste = false ,
412 this .selectAll = false ,
413 });
414
415 /// An instance of [ToolbarOptions ] with no options enabled.
416 static const ToolbarOptions empty = ToolbarOptions ();
417
418 /// Whether to show copy option in toolbar.
419 ///
420 /// Defaults to false.
421 final bool copy ;
422
423 /// Whether to show cut option in toolbar.
424 ///
425 /// If [EditableText .readOnly ] is set to true, cut will be disabled regardless.
426 ///
427 /// Defaults to false.
428 final bool cut ;
429
430 /// Whether to show paste option in toolbar.
431 ///
432 /// If [EditableText .readOnly ] is set to true, paste will be disabled regardless.
433 ///
434 /// Defaults to false.
435 final bool paste ;
436
437 /// Whether to show select all option in toolbar.
438 ///
439 /// Defaults to false.
440 final bool selectAll ;
441 }
442
443 /// Configures the ability to insert media content through the soft keyboard.
444 ///
445 /// The configuration provides a handler for any rich content inserted through
446 /// the system input method, and also provides the ability to limit the mime
447 /// types of the inserted content.
448 ///
449 /// See also:
450 ///
451 /// * [EditableText .contentInsertionConfiguration ]
452 class ContentInsertionConfiguration {
453 /// Creates a content insertion configuration with the specified options.
454 ///
455 /// A handler for inserted content, in the form of [onContentInserted ], must
456 /// be supplied.
457 ///
458 /// The allowable mime types of inserted content may also
459 /// be provided via [allowedMimeTypes ], which cannot be an empty list.
460 ContentInsertionConfiguration ({
461 required this .onContentInserted ,
462 this .allowedMimeTypes = kDefaultContentInsertionMimeTypes ,
463 }) : assert (allowedMimeTypes .isNotEmpty );
464
465 /// Called when a user inserts content through the virtual / on-screen keyboard,
466 /// currently only used on Android.
467 ///
468 /// [KeyboardInsertedContent ] holds the data representing the inserted content.
469 ///
470 /// {@tool dartpad}
471 ///
472 /// This example shows how to access the data for inserted content in your
473 /// `TextField`.
474 ///
475 /// ** See code in examples/api/lib/widgets/editable_text/editable_text.on_content_inserted.0.dart **
476 /// {@end-tool}
477 ///
478 /// See also:
479 ///
480 /// * <https://developer.android.com/guide/topics/text/image-keyboard>
481 final ValueChanged <KeyboardInsertedContent > onContentInserted ;
482
483 /// {@template flutter.widgets.contentInsertionConfiguration.allowedMimeTypes}
484 /// Used when a user inserts image-based content through the device keyboard,
485 /// currently only used on Android.
486 ///
487 /// The passed list of strings will determine which MIME types are allowed to
488 /// be inserted via the device keyboard.
489 ///
490 /// The default mime types are given by [kDefaultContentInsertionMimeTypes ].
491 /// These are all the mime types that are able to be handled and inserted
492 /// from keyboards.
493 ///
494 /// This field cannot be an empty list.
495 ///
496 /// {@tool dartpad}
497 /// This example shows how to limit image insertion to specific file types.
498 ///
499 /// ** See code in examples/api/lib/widgets/editable_text/editable_text.on_content_inserted.0.dart **
500 /// {@end-tool}
501 ///
502 /// See also:
503 ///
504 /// * <https://developer.android.com/guide/topics/text/image-keyboard>
505 /// {@endtemplate}
506 final List <String > allowedMimeTypes ;
507 }
508
509 // A time-value pair that represents a key frame in an animation.
510 class _KeyFrame {
511 const _KeyFrame (this .time , this .value );
512 // Values extracted from iOS 15.4 UIKit.
513 static const List <_KeyFrame > iOSBlinkingCaretKeyFrames = <_KeyFrame >[
514 _KeyFrame (0 , 1 ), // 0
515 _KeyFrame (0.5 , 1 ), // 1
516 _KeyFrame (0.5375 , 0.75 ), // 2
517 _KeyFrame (0.575 , 0.5 ), // 3
518 _KeyFrame (0.6125 , 0.25 ), // 4
519 _KeyFrame (0.65 , 0 ), // 5
520 _KeyFrame (0.85 , 0 ), // 6
521 _KeyFrame (0.8875 , 0.25 ), // 7
522 _KeyFrame (0.925 , 0.5 ), // 8
523 _KeyFrame (0.9625 , 0.75 ), // 9
524 _KeyFrame (1 , 1 ), // 10
525 ];
526
527 // The timing, in seconds, of the specified animation `value`.
528 final double time ;
529 final double value ;
530 }
531
532 class _DiscreteKeyFrameSimulation extends Simulation {
533 _DiscreteKeyFrameSimulation .iOSBlinkingCaret () : this ._ (_KeyFrame .iOSBlinkingCaretKeyFrames , 1 );
534 _DiscreteKeyFrameSimulation ._ (this ._keyFrames , this .maxDuration )
535 : assert (_keyFrames .isNotEmpty ),
536 assert (_keyFrames .last .time <= maxDuration ),
537 assert (() {
538 for (int i = 0 ; i < _keyFrames .length - 1 ; i += 1 ) {
539 if (_keyFrames [i ].time > _keyFrames [i + 1 ].time ) {
540 return false ;
541 }
542 }
543 return true ;
544 }(), 'The key frame sequence must be sorted by time.' );
545
546 final double maxDuration ;
547
548 final List <_KeyFrame > _keyFrames ;
549
550 @override
551 double dx (double time ) => 0 ;
552
553 @override
554 bool isDone (double time ) => time >= maxDuration ;
555
556 // The index of the KeyFrame corresponds to the most recent input `time`.
557 int _lastKeyFrameIndex = 0 ;
558
559 @override
560 double x (double time ) {
561 final int length = _keyFrames .length ;
562
563 // Perform a linear search in the sorted key frame list, starting from the
564 // last key frame found, since the input `time` usually monotonically
565 // increases by a small amount.
566 int searchIndex ;
567 final int endIndex ;
568 if (_keyFrames [_lastKeyFrameIndex ].time > time ) {
569 // The simulation may have restarted. Search within the index range
570 // [0, _lastKeyFrameIndex).
571 searchIndex = 0 ;
572 endIndex = _lastKeyFrameIndex ;
573 } else {
574 searchIndex = _lastKeyFrameIndex ;
575 endIndex = length ;
576 }
577
578 // Find the target key frame. Don't have to check (endIndex - 1): if
579 // (endIndex - 2) doesn't work we'll have to pick (endIndex - 1) anyways.
580 while (searchIndex < endIndex - 1 ) {
581 assert (_keyFrames [searchIndex ].time <= time );
582 final _KeyFrame next = _keyFrames [searchIndex + 1 ];
583 if (time < next .time ) {
584 break ;
585 }
586 searchIndex += 1 ;
587 }
588
589 _lastKeyFrameIndex = searchIndex ;
590 return _keyFrames [_lastKeyFrameIndex ].value ;
591 }
592 }
593
594 /// A basic text input field.
595 ///
596 /// This widget interacts with the [TextInput ] service to let the user edit the
597 /// text it contains. It also provides scrolling, selection, and cursor
598 /// movement.
599 ///
600 /// The [EditableText ] widget is a low-level widget that is intended as a
601 /// building block for custom widget sets. For a complete user experience,
602 /// consider using a [TextField] or [CupertinoTextField].
603 ///
604 /// ## Handling User Input
605 ///
606 /// Currently the user may change the text this widget contains via keyboard or
607 /// the text selection menu. When the user inserted or deleted text, you will be
608 /// notified of the change and get a chance to modify the new text value:
609 ///
610 /// * The [inputFormatters ] will be first applied to the user input.
611 ///
612 /// * The [controller ]'s [TextEditingController .value ] will be updated with the
613 /// formatted result, and the [controller ]'s listeners will be notified.
614 ///
615 /// * The [onChanged ] callback, if specified, will be called last.
616 ///
617 /// ## Input Actions
618 ///
619 /// A [TextInputAction ] can be provided to customize the appearance of the
620 /// action button on the soft keyboard for Android and iOS. The default action
621 /// is [TextInputAction .done ].
622 ///
623 /// Many [TextInputAction ]s are common between Android and iOS. However, if a
624 /// [textInputAction ] is provided that is not supported by the current
625 /// platform in debug mode, an error will be thrown when the corresponding
626 /// EditableText receives focus. For example, providing iOS's "emergencyCall"
627 /// action when running on an Android device will result in an error when in
628 /// debug mode. In release mode, incompatible [TextInputAction ]s are replaced
629 /// either with "unspecified" on Android, or "default" on iOS. Appropriate
630 /// [textInputAction ]s can be chosen by checking the current platform and then
631 /// selecting the appropriate action.
632 ///
633 /// {@template flutter.widgets.EditableText.lifeCycle}
634 /// ## Lifecycle
635 ///
636 /// Upon completion of editing, like pressing the "done" button on the keyboard,
637 /// two actions take place:
638 ///
639 /// 1st: Editing is finalized. The default behavior of this step includes
640 /// an invocation of [onChanged ]. That default behavior can be overridden.
641 /// See [onEditingComplete ] for details.
642 ///
643 /// 2nd: [onSubmitted ] is invoked with the user's input value.
644 ///
645 /// [onSubmitted ] can be used to manually move focus to another input widget
646 /// when a user finishes with the currently focused input widget.
647 ///
648 /// When the widget has focus, it will prevent itself from disposing via
649 /// [AutomaticKeepAliveClientMixin .wantKeepAlive ] in order to avoid losing the
650 /// selection. Removing the focus will allow it to be disposed.
651 /// {@endtemplate}
652 ///
653 /// Rather than using this widget directly, consider using [TextField], which
654 /// is a full-featured, material-design text input field with placeholder text,
655 /// labels, and [Form] integration.
656 ///
657 /// ## Text Editing [Intent ]s and Their Default [Action ]s
658 ///
659 /// This widget provides default [Action ]s for handling common text editing
660 /// [Intent ]s such as deleting, copying and pasting in the text field. These
661 /// [Action ]s can be directly invoked using [Actions .invoke ] or the
662 /// [Actions .maybeInvoke ] method. The default text editing keyboard [Shortcuts ],
663 /// typically declared in [DefaultTextEditingShortcuts ], also use these
664 /// [Intent ]s and [Action ]s to perform the text editing operations they are
665 /// bound to.
666 ///
667 /// The default handling of a specific [Intent ] can be overridden by placing an
668 /// [Actions ] widget above this widget. See the [Action ] class and the
669 /// [Action .overridable ] constructor for more information on how a pre-defined
670 /// overridable [Action ] can be overridden.
671 ///
672 /// ### Intents for Deleting Text and Their Default Behavior
673 ///
674 /// | **Intent Class** | **Default Behavior when there's selected text** | **Default Behavior when there is a [caret](https://en.wikipedia.org/wiki/Caret_navigation) (The selection is [TextSelection.collapsed])** |
675 /// | :------------------------------- | :--------------------------------------------------- | :----------------------------------------------------------------------- |
676 /// | [DeleteCharacterIntent] | Deletes the selected text | Deletes the user-perceived character before or after the caret location. |
677 /// | [DeleteToNextWordBoundaryIntent] | Deletes the selected text and the word before/after the selection's [TextSelection.extent] position | Deletes from the caret location to the previous or the next word boundary |
678 /// | [DeleteToLineBreakIntent] | Deletes the selected text, and deletes to the start/end of the line from the selection's [TextSelection.extent] position | Deletes from the caret location to the logical start or end of the current line |
679 ///
680 /// ### Intents for Moving the [Caret](https://en.wikipedia.org/wiki/Caret_navigation)
681 ///
682 /// | **Intent Class** | **Default Behavior when there's selected text** | **Default Behavior when there is a caret ([TextSelection.collapsed])** |
683 /// | :----------------------------------------------------------------------------------- | :--------------------------------------------------------------- | :---------------------------------------------------------------------- |
684 /// | [ExtendSelectionByCharacterIntent](`collapseSelection: true`) | Collapses the selection to the logical start/end of the selection | Moves the caret past the user-perceived character before or after the current caret location. |
685 /// | [ExtendSelectionToNextWordBoundaryIntent](`collapseSelection: true`) | Collapses the selection to the word boundary before/after the selection's [TextSelection.extent] position | Moves the caret to the previous/next word boundary. |
686 /// | [ExtendSelectionToNextWordBoundaryOrCaretLocationIntent](`collapseSelection: true`) | Collapses the selection to the word boundary before/after the selection's [TextSelection.extent] position, or [TextSelection.base], whichever is closest in the given direction | Moves the caret to the previous/next word boundary. |
687 /// | [ExtendSelectionToLineBreakIntent](`collapseSelection: true`) | Collapses the selection to the start/end of the line at the selection's [TextSelection.extent] position | Moves the caret to the start/end of the current line .|
688 /// | [ExtendSelectionVerticallyToAdjacentLineIntent](`collapseSelection: true`) | Collapses the selection to the position closest to the selection's [TextSelection.extent], on the previous/next adjacent line | Moves the caret to the closest position on the previous/next adjacent line. |
689 /// | [ExtendSelectionVerticallyToAdjacentPageIntent](`collapseSelection: true`) | Collapses the selection to the position closest to the selection's [TextSelection.extent], on the previous/next adjacent page | Moves the caret to the closest position on the previous/next adjacent page. |
690 /// | [ExtendSelectionToDocumentBoundaryIntent](`collapseSelection: true`) | Collapses the selection to the start/end of the document | Moves the caret to the start/end of the document. |
691 ///
692 /// #### Intents for Extending the Selection
693 ///
694 /// | **Intent Class** | **Default Behavior when there's selected text** | **Default Behavior when there is a caret ([TextSelection.collapsed])** |
695 /// | :----------------------------------------------------------------------------------- | :--------------------------------------------------------------- | :---------------------------------------------------------------------- |
696 /// | [ExtendSelectionByCharacterIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] past the user-perceived character before/after it |
697 /// | [ExtendSelectionToNextWordBoundaryIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] to the previous/next word boundary |
698 /// | [ExtendSelectionToNextWordBoundaryOrCaretLocationIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] to the previous/next word boundary, or [TextSelection.base] whichever is closest in the given direction | Moves the selection's [TextSelection.extent] to the previous/next word boundary. |
699 /// | [ExtendSelectionToLineBreakIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] to the start/end of the line |
700 /// | [ExtendSelectionVerticallyToAdjacentLineIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] to the closest position on the previous/next adjacent line |
701 /// | [ExtendSelectionVerticallyToAdjacentPageIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] to the closest position on the previous/next adjacent page |
702 /// | [ExtendSelectionToDocumentBoundaryIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] to the start/end of the document |
703 /// | [SelectAllTextIntent] | Selects the entire document |
704 ///
705 /// ### Other Intents
706 ///
707 /// | **Intent Class** | **Default Behavior** |
708 /// | :-------------------------------------- | :--------------------------------------------------- |
709 /// | [DoNothingAndStopPropagationTextIntent] | Does nothing in the input field, and prevents the key event from further propagating in the widget tree. |
710 /// | [ReplaceTextIntent] | Replaces the current [TextEditingValue] in the input field's [TextEditingController], and triggers all related user callbacks and [TextInputFormatter]s. |
711 /// | [UpdateSelectionIntent] | Updates the current selection in the input field's [TextEditingController], and triggers the [onSelectionChanged] callback. |
712 /// | [CopySelectionTextIntent] | Copies or cuts the selected text into the clipboard |
713 /// | [PasteTextIntent] | Inserts the current text in the clipboard after the caret location, or replaces the selected text if the selection is not collapsed. |
714 ///
715 /// ## Text Editing [Shortcuts]
716 ///
717 /// It's also possible to directly remap keyboard shortcuts to new [Intent]s by
718 /// inserting a [Shortcuts] widget above this in the widget tree. When using
719 /// [WidgetsApp], the large set of default text editing keyboard shortcuts are
720 /// declared near the top of the widget tree in [DefaultTextEditingShortcuts],
721 /// and any [Shortcuts] widget between it and this [EditableText] will override
722 /// those defaults.
723 ///
724 /// {@template flutter.widgets.editableText.shortcutsAndTextInput}
725 /// ### Interactions Between [Shortcuts] and Text Input
726 ///
727 /// Shortcuts prevent text input fields from receiving their keystrokes as text
728 /// input. For example, placing a [Shortcuts] widget in the widget tree above
729 /// a text input field and creating a shortcut for [LogicalKeyboardKey.keyA]
730 /// will prevent the field from receiving that key as text input. In other
731 /// words, typing key "A" into the field will trigger the shortcut and will not
732 /// insert a letter "a" into the field.
733 ///
734 /// This happens because of the way that key strokes are handled in Flutter.
735 /// When a keystroke is received in Flutter's engine, it first gives the
736 /// framework the opportunity to handle it as a raw key event through
737 /// [SystemChannels.keyEvent]. This is what [Shortcuts] listens to indirectly
738 /// through its [FocusNode]. If it is not handled, then it will proceed to try
739 /// handling it as text input through [SystemChannels.textInput], which is what
740 /// [EditableTextState] listens to through [TextInputClient].
741 ///
742 /// This behavior, where a shortcut prevents text input into some field, can be
743 /// overridden by using another [Shortcuts] widget lower in the widget tree and
744 /// mapping the desired key stroke(s) to [DoNothingAndStopPropagationIntent].
745 /// The key event will be reported as unhandled by the framework and will then
746 /// be sent as text input as usual.
747 /// {@endtemplate}
748 ///
749 /// ## Gesture Events Handling
750 ///
751 /// When [rendererIgnoresPointer] is false (the default), this widget provides
752 /// rudimentary, platform-agnostic gesture handling for user actions such as
753 /// tapping, long-pressing, and scrolling.
754 ///
755 /// To provide more complete gesture handling, including double-click to select
756 /// a word, drag selection, and platform-specific handling of gestures such as
757 /// long presses, consider setting [rendererIgnoresPointer] to true and using
758 /// [TextSelectionGestureDetectorBuilder].
759 ///
760 /// {@template flutter.widgets.editableText.showCaretOnScreen}
761 /// ## Keep the caret visible when focused
762 ///
763 /// When focused, this widget will make attempts to keep the text area and its
764 /// caret (even when [showCursor] is `false`) visible, on these occasions:
765 ///
766 /// * When the user focuses this text field and it is not [readOnly].
767 /// * When the user changes the selection of the text field, or changes the
768 /// text when the text field is not [readOnly].
769 /// * When the virtual keyboard pops up.
770 /// {@endtemplate}
771 ///
772 /// ## Scrolling Considerations
773 ///
774 /// If this [EditableText] is not a descendant of [Scaffold] and is being used
775 /// within a [Scrollable] or nested [Scrollable]s, consider placing a
776 /// [ScrollNotificationObserver] above the root [Scrollable] that contains this
777 /// [EditableText] to ensure proper scroll coordination for [EditableText] and
778 /// its components like [TextSelectionOverlay].
779 ///
780 /// {@template flutter.widgets.editableText.accessibility}
781 /// ## Troubleshooting Common Accessibility Issues
782 ///
783 /// ### Customizing User Input Accessibility Announcements
784 ///
785 /// To customize user input accessibility announcements triggered by text
786 /// changes, use [SemanticsService.announce] to make the desired
787 /// accessibility announcement.
788 ///
789 /// On iOS, the on-screen keyboard may announce the most recent input
790 /// incorrectly when a [TextInputFormatter] inserts a thousands separator to
791 /// a currency value text field. The following example demonstrates how to
792 /// suppress the default accessibility announcements by always announcing
793 /// the content of the text field as a US currency value (the `\$` inserts
794 /// a dollar sign, the `$newText` interpolates the `newText` variable):
795 ///
796 /// ```dart
797 /// onChanged: (String newText) {
798 /// if (newText.isNotEmpty) {
799 /// SemanticsService.announce('\$$newText', Directionality.of(context));
800 /// }
801 /// }
802 /// ```
803 ///
804 /// {@endtemplate}
805 ///
806 /// See also:
807 ///
808 /// * [TextField], which is a full-featured, material-design text input field
809 /// with placeholder text, labels, and [Form] integration.
810 class EditableText extends StatefulWidget {
811 /// Creates a basic text input control.
812 ///
813 /// The [maxLines] property can be set to null to remove the restriction on
814 /// the number of lines. By default, it is one, meaning this is a single-line
815 /// text field. [maxLines] must be null or greater than zero.
816 ///
817 /// If [keyboardType] is not set or is null, its value will be inferred from
818 /// [autofillHints], if [autofillHints] is not empty. Otherwise it defaults to
819 /// [TextInputType.text] if [maxLines] is exactly one, and
820 /// [TextInputType.multiline] if [maxLines] is null or greater than one.
821 ///
822 /// The text cursor is not shown if [showCursor] is false or if [showCursor]
823 /// is null (the default) and [readOnly] is true.
824 EditableText({
825 super.key,
826 required this.controller,
827 required this.focusNode,
828 this.readOnly = false,
829 this.obscuringCharacter = '•',
830 this.obscureText = false,
831 bool? autocorrect,
832 SmartDashesType? smartDashesType,
833 SmartQuotesType? smartQuotesType,
834 this.enableSuggestions = true,
835 required this.style,
836 StrutStyle? strutStyle,
837 required this.cursorColor,
838 required this.backgroundCursorColor,
839 this.textAlign = TextAlign.start,
840 this.textDirection,
841 this.locale,
842 @Deprecated(
843 'Use textScaler instead. '
844 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
845 'This feature was deprecated after v3.12.0-2.0.pre.',
846 )
847 this.textScaleFactor,
848 this.textScaler,
849 this.maxLines = 1,
850 this.minLines,
851 this.expands = false,
852 this.forceLine = true,
853 this.textHeightBehavior,
854 this.textWidthBasis = TextWidthBasis.parent,
855 this.autofocus = false,
856 bool? showCursor,
857 this.showSelectionHandles = false,
858 this.selectionColor,
859 this.selectionControls,
860 TextInputType? keyboardType,
861 this.textInputAction,
862 this.textCapitalization = TextCapitalization.none,
863 this.onChanged,
864 this.onEditingComplete,
865 this.onSubmitted,
866 this.onAppPrivateCommand,
867 this.onSelectionChanged,
868 this.onSelectionHandleTapped,
869 this.groupId = EditableText,
870 this.onTapOutside,
871 this.onTapUpOutside,
872 List<TextInputFormatter>? inputFormatters,
873 this.mouseCursor,
874 this.rendererIgnoresPointer = false,
875 this.cursorWidth = 2.0,
876 this.cursorHeight,
877 this.cursorRadius,
878 this.cursorOpacityAnimates = false,
879 this.cursorOffset,
880 this.paintCursorAboveText = false,
881 ui.BoxHeightStyle? selectionHeightStyle,
882 ui.BoxWidthStyle? selectionWidthStyle,
883 this.scrollPadding = const EdgeInsets.all(20.0),
884 this.keyboardAppearance = Brightness.light,
885 this.dragStartBehavior = DragStartBehavior.start,
886 bool? enableInteractiveSelection,
887 bool? selectAllOnFocus,
888 this.scrollController,
889 this.scrollPhysics,
890 this.autocorrectionTextRectColor,
891 @Deprecated(
892 'Use `contextMenuBuilder` instead. '
893 'This feature was deprecated after v3.3.0-0.5.pre.',
894 )
895 ToolbarOptions? toolbarOptions,
896 this.autofillHints = const <String>[],
897 this.autofillClient,
898 this.clipBehavior = Clip.hardEdge,
899 this.restorationId,
900 this.scrollBehavior,
901 @Deprecated(
902 'Use `stylusHandwritingEnabled` instead. '
903 'This feature was deprecated after v3.27.0-0.2.pre.',
904 )
905 this.scribbleEnabled = true,
906 this.stylusHandwritingEnabled = defaultStylusHandwritingEnabled,
907 this.enableIMEPersonalizedLearning = true,
908 this.contentInsertionConfiguration,
909 this.contextMenuBuilder,
910 this.spellCheckConfiguration,
911 this.magnifierConfiguration = TextMagnifierConfiguration.disabled,
912 this.undoController,
913 this.hintLocales,
914 }) : assert(obscuringCharacter.length == 1),
915 autocorrect = autocorrect ?? _inferAutocorrect(autofillHints: autofillHints),
916 smartDashesType =
917 smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
918 smartQuotesType =
919 smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
920 assert(minLines == null || minLines > 0),
921 assert(
922 (maxLines == null) || (minLines == null) || (maxLines >= minLines),
923 "minLines can't be greater than maxLines",
924 ),
925 assert(
926 !expands || (maxLines == null && minLines == null),
927 'minLines and maxLines must be null when expands is true.',
928 ),
929 assert(!obscureText || maxLines == 1, 'Obscured fields cannot be multiline.'),
930 enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText),
931 selectAllOnFocus = selectAllOnFocus ?? _defaultSelectAllOnFocus,
932 toolbarOptions = selectionControls is TextSelectionHandleControls && toolbarOptions == null
933 ? ToolbarOptions.empty
934 : toolbarOptions ??
935 (obscureText
936 ? (readOnly
937 // No point in even offering "Select All" in a read-only obscured
938 // field.
939 ? ToolbarOptions.empty
940 // Writable, but obscured.
941 : const ToolbarOptions(selectAll: true, paste: true))
942 : (readOnly
943 // Read-only, not obscured.
944 ? const ToolbarOptions(selectAll: true, copy: true)
945 // Writable, not obscured.
946 : const ToolbarOptions(
947 copy: true,
948 cut: true,
949 selectAll: true,
950 paste: true,
951 ))),
952 assert(
953 spellCheckConfiguration == null ||
954 spellCheckConfiguration == const SpellCheckConfiguration.disabled() ||
955 spellCheckConfiguration.misspelledTextStyle != null,
956 'spellCheckConfiguration must specify a misspelledTextStyle if spell check behavior is desired',
957 ),
958 _strutStyle = strutStyle,
959 keyboardType =
960 keyboardType ?? _inferKeyboardType(autofillHints: autofillHints, maxLines: maxLines),
961 inputFormatters = maxLines == 1
962 ? <TextInputFormatter>[
963 FilteringTextInputFormatter.singleLineFormatter,
964 ...inputFormatters ?? const Iterable<TextInputFormatter>.empty(),
965 ]
966 : inputFormatters,
967 showCursor = showCursor ?? !readOnly,
968 selectionHeightStyle = selectionHeightStyle ?? defaultSelectionHeightStyle,
969 selectionWidthStyle = selectionWidthStyle ?? defaultSelectionWidthStyle;
970
971 /// Controls the text being edited.
972 final TextEditingController controller;
973
974 /// Controls whether this widget has keyboard focus.
975 final FocusNode focusNode;
976
977 /// {@template flutter.widgets.editableText.obscuringCharacter}
978 /// Character used for obscuring text if [obscureText] is true.
979 ///
980 /// Must be only a single character.
981 ///
982 /// Defaults to the character U+2022 BULLET (•).
983 /// {@endtemplate}
984 final String obscuringCharacter;
985
986 /// {@template flutter.widgets.editableText.obscureText}
987 /// Whether to hide the text being edited (e.g., for passwords).
988 ///
989 /// When this is set to true, all the characters in the text field are
990 /// replaced by [obscuringCharacter], and the text in the field cannot be
991 /// copied with copy or cut. If [readOnly] is also true, then the text cannot
992 /// be selected.
993 ///
994 /// Defaults to false.
995 /// {@endtemplate}
996 final bool obscureText;
997
998 /// {@macro dart.ui.textHeightBehavior}
999 final TextHeightBehavior? textHeightBehavior;
1000
1001 /// {@macro flutter.painting.textPainter.textWidthBasis}
1002 final TextWidthBasis textWidthBasis;
1003
1004 /// {@template flutter.widgets.editableText.readOnly}
1005 /// Whether the text can be changed.
1006 ///
1007 /// When this is set to true, the text cannot be modified
1008 /// by any shortcut or keyboard operation. The text is still selectable.
1009 ///
1010 /// Defaults to false.
1011 /// {@endtemplate}
1012 final bool readOnly;
1013
1014 /// Whether the text will take the full width regardless of the text width.
1015 ///
1016 /// When this is set to false, the width will be based on text width, which
1017 /// will also be affected by [textWidthBasis].
1018 ///
1019 /// Defaults to true.
1020 ///
1021 /// See also:
1022 ///
1023 /// * [textWidthBasis], which controls the calculation of text width.
1024 final bool forceLine;
1025
1026 /// Configuration of toolbar options.
1027 ///
1028 /// By default, all options are enabled. If [readOnly] is true, paste and cut
1029 /// will be disabled regardless. If [obscureText] is true, cut and copy will
1030 /// be disabled regardless. If [readOnly] and [obscureText] are both true,
1031 /// select all will also be disabled.
1032 final ToolbarOptions toolbarOptions;
1033
1034 /// Whether to show selection handles.
1035 ///
1036 /// When a selection is active, there will be two handles at each side of
1037 /// boundary, or one handle if the selection is collapsed. The handles can be
1038 /// dragged to adjust the selection.
1039 ///
1040 /// See also:
1041 ///
1042 /// * [showCursor], which controls the visibility of the cursor.
1043 final bool showSelectionHandles;
1044
1045 /// {@template flutter.widgets.editableText.showCursor}
1046 /// Whether to show cursor.
1047 ///
1048 /// The cursor refers to the blinking caret when the [EditableText] is focused.
1049 /// {@endtemplate}
1050 ///
1051 /// See also:
1052 ///
1053 /// * [showSelectionHandles], which controls the visibility of the selection handles.
1054 final bool showCursor;
1055
1056 /// {@template flutter.widgets.editableText.autocorrect}
1057 /// Whether to enable autocorrection.
1058 ///
1059 /// False on iOS if [autofillHints] contains password-related hints, otherwise true.
1060 /// {@endtemplate}
1061 final bool autocorrect;
1062
1063 /// {@macro flutter.services.TextInputConfiguration.smartDashesType}
1064 final SmartDashesType smartDashesType;
1065
1066 /// {@macro flutter.services.TextInputConfiguration.smartQuotesType}
1067 final SmartQuotesType smartQuotesType;
1068
1069 /// {@macro flutter.services.TextInputConfiguration.enableSuggestions}
1070 final bool enableSuggestions;
1071
1072 /// The text style to use for the editable text.
1073 final TextStyle style;
1074
1075 /// Controls the undo state of the current editable text.
1076 ///
1077 /// If null, this widget will create its own [UndoHistoryController].
1078 final UndoHistoryController? undoController;
1079
1080 /// {@template flutter.widgets.editableText.strutStyle}
1081 /// The strut style used for the vertical layout.
1082 ///
1083 /// [StrutStyle] is used to establish a predictable vertical layout.
1084 /// Since fonts may vary depending on user input and due to font
1085 /// fallback, [StrutStyle.forceStrutHeight] is enabled by default
1086 /// to lock all lines to the height of the base [TextStyle], provided by
1087 /// [style]. This ensures the typed text fits within the allotted space.
1088 ///
1089 /// If null, the strut used will inherit values from the [style] and will
1090 /// have [StrutStyle.forceStrutHeight] set to true. When no [style] is
1091 /// passed, the theme's [TextStyle] will be used to generate [strutStyle]
1092 /// instead.
1093 ///
1094 /// To disable strut-based vertical alignment and allow dynamic vertical
1095 /// layout based on the glyphs typed, use [StrutStyle.disabled].
1096 ///
1097 /// Flutter's strut is based on [typesetting strut](https://en.wikipedia.org/wiki/Strut_(typesetting))
1098 /// and CSS's [line-height](https://www.w3.org/TR/CSS2/visudet.html#line-height).
1099 /// {@endtemplate}
1100 ///
1101 /// Within editable text and text fields, [StrutStyle] will not use its standalone
1102 /// default values, and will instead inherit omitted/null properties from the
1103 /// [TextStyle] instead. See [StrutStyle.inheritFromTextStyle].
1104 StrutStyle get strutStyle {
1105 if (_strutStyle == null) {
1106 return StrutStyle.fromTextStyle(style, forceStrutHeight: true);
1107 }
1108 return _strutStyle.inheritFromTextStyle(style);
1109 }
1110
1111 final StrutStyle? _strutStyle;
1112
1113 /// {@template flutter.widgets.editableText.textAlign}
1114 /// How the text should be aligned horizontally.
1115 ///
1116 /// Defaults to [TextAlign.start].
1117 /// {@endtemplate}
1118 final TextAlign textAlign;
1119
1120 /// {@template flutter.widgets.editableText.textDirection}
1121 /// The directionality of the text.
1122 ///
1123 /// This decides how [textAlign] values like [TextAlign.start] and
1124 /// [TextAlign.end] are interpreted.
1125 ///
1126 /// This is also used to disambiguate how to render bidirectional text. For
1127 /// example, if the text is an English phrase followed by a Hebrew phrase,
1128 /// in a [TextDirection.ltr] context the English phrase will be on the left
1129 /// and the Hebrew phrase to its right, while in a [TextDirection.rtl]
1130 /// context, the English phrase will be on the right and the Hebrew phrase on
1131 /// its left.
1132 ///
1133 /// Defaults to the ambient [Directionality], if any.
1134 /// {@endtemplate}
1135 final TextDirection? textDirection;
1136
1137 /// {@template flutter.widgets.editableText.textCapitalization}
1138 /// Configures how the platform keyboard will select an uppercase or
1139 /// lowercase keyboard.
1140 ///
1141 /// Only supports text keyboards, other keyboard types will ignore this
1142 /// configuration. Capitalization is locale-aware.
1143 ///
1144 /// Defaults to [TextCapitalization.none].
1145 ///
1146 /// See also:
1147 ///
1148 /// * [TextCapitalization], for a description of each capitalization behavior.
1149 ///
1150 /// {@endtemplate}
1151 final TextCapitalization textCapitalization;
1152
1153 /// Used to select a font when the same Unicode character can
1154 /// be rendered differently, depending on the locale.
1155 ///
1156 /// It's rarely necessary to set this property. By default its value
1157 /// is inherited from the enclosing app with `Localizations.localeOf(context)`.
1158 ///
1159 /// See [RenderEditable.locale] for more information.
1160 final Locale? locale;
1161
1162 /// {@template flutter.widgets.editableText.textScaleFactor}
1163 /// Deprecated. Will be removed in a future version of Flutter. Use
1164 /// [textScaler] instead.
1165 ///
1166 /// The number of font pixels for each logical pixel.
1167 ///
1168 /// For example, if the text scale factor is 1.5, text will be 50% larger than
1169 /// the specified font size.
1170 ///
1171 /// Defaults to the [MediaQueryData.textScaleFactor] obtained from the ambient
1172 /// [MediaQuery], or 1.0 if there is no [MediaQuery] in scope.
1173 /// {@endtemplate}
1174 @Deprecated(
1175 'Use textScaler instead. '
1176 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
1177 'This feature was deprecated after v3.12.0-2.0.pre.',
1178 )
1179 final double? textScaleFactor;
1180
1181 /// {@macro flutter.painting.textPainter.textScaler}
1182 final TextScaler? textScaler;
1183
1184 /// The color to use when painting the cursor.
1185 final Color cursorColor;
1186
1187 /// The color to use when painting the autocorrection Rect.
1188 ///
1189 /// For [CupertinoTextField]s, the value is set to the ambient
1190 /// [CupertinoThemeData.primaryColor] with 20% opacity. For [TextField]s, the
1191 /// value is null on non-iOS platforms and the same color used in [CupertinoTextField]
1192 /// on iOS.
1193 ///
1194 /// Currently the autocorrection Rect only appears on iOS.
1195 ///
1196 /// Defaults to null, which disables autocorrection Rect painting.
1197 final Color? autocorrectionTextRectColor;
1198
1199 /// The color to use when painting the background cursor aligned with the text
1200 /// while rendering the floating cursor.
1201 ///
1202 /// Typically this would be set to [CupertinoColors.inactiveGray].
1203 ///
1204 /// See also:
1205 ///
1206 /// * [FloatingCursorDragState], which explains the floating cursor feature
1207 /// in detail.
1208 final Color backgroundCursorColor;
1209
1210 /// {@template flutter.widgets.editableText.maxLines}
1211 /// The maximum number of lines to show at one time, wrapping if necessary.
1212 ///
1213 /// This affects the height of the field itself and does not limit the number
1214 /// of lines that can be entered into the field.
1215 ///
1216 /// If this is 1 (the default), the text will not wrap, but will scroll
1217 /// horizontally instead.
1218 ///
1219 /// If this is null, there is no limit to the number of lines, and the text
1220 /// container will start with enough vertical space for one line and
1221 /// automatically grow to accommodate additional lines as they are entered, up
1222 /// to the height of its constraints.
1223 ///
1224 /// If this is not null, the value must be greater than zero, and it will lock
1225 /// the input to the given number of lines and take up enough horizontal space
1226 /// to accommodate that number of lines. Setting [minLines] as well allows the
1227 /// input to grow and shrink between the indicated range.
1228 ///
1229 /// The full set of behaviors possible with [minLines] and [maxLines] are as
1230 /// follows. These examples apply equally to [TextField], [TextFormField],
1231 /// [CupertinoTextField], and [EditableText].
1232 ///
1233 /// Input that occupies a single line and scrolls horizontally as needed.
1234 /// ```dart
1235 /// const TextField()
1236 /// ```
1237 ///
1238 /// Input whose height grows from one line up to as many lines as needed for
1239 /// the text that was entered. If a height limit is imposed by its parent, it
1240 /// will scroll vertically when its height reaches that limit.
1241 /// ```dart
1242 /// const TextField(maxLines: null)
1243 /// ```
1244 ///
1245 /// The input's height is large enough for the given number of lines. If
1246 /// additional lines are entered the input scrolls vertically.
1247 /// ```dart
1248 /// const TextField(maxLines: 2)
1249 /// ```
1250 ///
1251 /// Input whose height grows with content between a min and max. An infinite
1252 /// max is possible with `maxLines: null`.
1253 /// ```dart
1254 /// const TextField(minLines: 2, maxLines: 4)
1255 /// ```
1256 ///
1257 /// See also:
1258 ///
1259 /// * [minLines], which sets the minimum number of lines visible.
1260 /// {@endtemplate}
1261 /// * [expands], which determines whether the field should fill the height of
1262 /// its parent.
1263 final int? maxLines;
1264
1265 /// {@template flutter.widgets.editableText.minLines}
1266 /// The minimum number of lines to occupy when the content spans fewer lines.
1267 ///
1268 /// This affects the height of the field itself and does not limit the number
1269 /// of lines that can be entered into the field.
1270 ///
1271 /// If this is null (default), text container starts with enough vertical space
1272 /// for one line and grows to accommodate additional lines as they are entered.
1273 ///
1274 /// This can be used in combination with [maxLines] for a varying set of behaviors.
1275 ///
1276 /// If the value is set, it must be greater than zero. If the value is greater
1277 /// than 1, [maxLines] should also be set to either null or greater than
1278 /// this value.
1279 ///
1280 /// When [maxLines] is set as well, the height will grow between the indicated
1281 /// range of lines. When [maxLines] is null, it will grow as high as needed,
1282 /// starting from [minLines].
1283 ///
1284 /// A few examples of behaviors possible with [minLines] and [maxLines] are as follows.
1285 /// These apply equally to [TextField], [TextFormField], [CupertinoTextField],
1286 /// and [EditableText].
1287 ///
1288 /// Input that always occupies at least 2 lines and has an infinite max.
1289 /// Expands vertically as needed.
1290 /// ```dart
1291 /// TextField(minLines: 2)
1292 /// ```
1293 ///
1294 /// Input whose height starts from 2 lines and grows up to 4 lines at which
1295 /// point the height limit is reached. If additional lines are entered it will
1296 /// scroll vertically.
1297 /// ```dart
1298 /// const TextField(minLines:2, maxLines: 4)
1299 /// ```
1300 ///
1301 /// Defaults to null.
1302 ///
1303 /// See also:
1304 ///
1305 /// * [maxLines], which sets the maximum number of lines visible, and has
1306 /// several examples of how minLines and maxLines interact to produce
1307 /// various behaviors.
1308 /// {@endtemplate}
1309 /// * [expands], which determines whether the field should fill the height of
1310 /// its parent.
1311 final int? minLines;
1312
1313 /// {@template flutter.widgets.editableText.expands}
1314 /// Whether this widget's height will be sized to fill its parent.
1315 ///
1316 /// If set to true and wrapped in a parent widget like [Expanded] or
1317 /// [SizedBox], the input will expand to fill the parent.
1318 ///
1319 /// [maxLines] and [minLines] must both be null when this is set to true,
1320 /// otherwise an error is thrown.
1321 ///
1322 /// Defaults to false.
1323 ///
1324 /// See the examples in [maxLines] for the complete picture of how [maxLines],
1325 /// [minLines], and [expands] interact to produce various behaviors.
1326 ///
1327 /// Input that matches the height of its parent:
1328 /// ```dart
1329 /// const Expanded(
1330 /// child: TextField(maxLines: null, expands: true),
1331 /// )
1332 /// ```
1333 /// {@endtemplate}
1334 final bool expands;
1335
1336 /// {@template flutter.widgets.editableText.autofocus}
1337 /// Whether this text field should focus itself if nothing else is already
1338 /// focused.
1339 ///
1340 /// If true, the keyboard will open as soon as this text field obtains focus.
1341 /// Otherwise, the keyboard is only shown after the user taps the text field.
1342 ///
1343 /// Defaults to false.
1344 /// {@endtemplate}
1345 // See https://github.com/flutter/flutter/issues/7035 for the rationale for this
1346 // keyboard behavior.
1347 final bool autofocus;
1348
1349 /// The color to use when painting the selection.
1350 ///
1351 /// If this property is null, this widget gets the selection color from the
1352 /// [DefaultSelectionStyle].
1353 ///
1354 /// For [CupertinoTextField]s, the value is set to the ambient
1355 /// [CupertinoThemeData.primaryColor] with 20% opacity. For [TextField]s, the
1356 /// value is set to the ambient [TextSelectionThemeData.selectionColor].
1357 final Color? selectionColor;
1358
1359 /// {@template flutter.widgets.editableText.selectionControls}
1360 /// Optional delegate for building the text selection handles.
1361 ///
1362 /// Historically, this field also controlled the toolbar. This is now handled
1363 /// by [contextMenuBuilder] instead. However, for backwards compatibility, when
1364 /// [selectionControls] is set to an object that does not mix in
1365 /// [TextSelectionHandleControls], [contextMenuBuilder] is ignored and the
1366 /// [TextSelectionControls.buildToolbar] method is used instead.
1367 /// {@endtemplate}
1368 ///
1369 /// See also:
1370 ///
1371 /// * [CupertinoTextField], which wraps an [EditableText] and which shows the
1372 /// selection toolbar upon user events that are appropriate on the iOS
1373 /// platform.
1374 /// * [TextField], a Material Design themed wrapper of [EditableText], which
1375 /// shows the selection toolbar upon appropriate user events based on the
1376 /// user's platform set in [ThemeData.platform].
1377 final TextSelectionControls? selectionControls;
1378
1379 /// {@template flutter.widgets.editableText.keyboardType}
1380 /// The type of keyboard to use for editing the text.
1381 ///
1382 /// Defaults to [TextInputType.text] if [maxLines] is one and
1383 /// [TextInputType.multiline] otherwise.
1384 /// {@endtemplate}
1385 final TextInputType keyboardType;
1386
1387 /// The type of action button to use with the soft keyboard.
1388 final TextInputAction? textInputAction;
1389
1390 /// {@template flutter.widgets.editableText.onChanged}
1391 /// Called when the user initiates a change to the TextField's
1392 /// value: when they have inserted or deleted text.
1393 ///
1394 /// This callback doesn't run when the TextField's text is changed
1395 /// programmatically, via the TextField's [controller]. Typically it
1396 /// isn't necessary to be notified of such changes, since they're
1397 /// initiated by the app itself.
1398 ///
1399 /// To be notified of all changes to the TextField's text, cursor,
1400 /// and selection, one can add a listener to its [controller] with
1401 /// [TextEditingController.addListener].
1402 ///
1403 /// [onChanged] is called before [onSubmitted] when user indicates completion
1404 /// of editing, such as when pressing the "done" button on the keyboard. That
1405 /// default behavior can be overridden. See [onEditingComplete] for details.
1406 ///
1407 /// {@tool dartpad}
1408 /// This example shows how onChanged could be used to check the TextField's
1409 /// current value each time the user inserts or deletes a character.
1410 ///
1411 /// ** See code in examples/api/lib/widgets/editable_text/editable_text.on_changed.0.dart **
1412 /// {@end-tool}
1413 /// {@endtemplate}
1414 ///
1415 /// ## Handling emojis and other complex characters
1416 /// {@template flutter.widgets.EditableText.onChanged}
1417 /// It's important to always use
1418 /// [characters](https://pub.dev/packages/characters) when dealing with user
1419 /// input text that may contain complex characters. This will ensure that
1420 /// extended grapheme clusters and surrogate pairs are treated as single
1421 /// characters, as they appear to the user.
1422 ///
1423 /// For example, when finding the length of some user input, use
1424 /// `string.characters.length`. Do NOT use `string.length` or even
1425 /// `string.runes.length`. For the complex character "👨👩👦", this
1426 /// appears to the user as a single character, and `string.characters.length`
1427 /// intuitively returns 1. On the other hand, `string.length` returns 8, and
1428 /// `string.runes.length` returns 5!
1429 /// {@endtemplate}
1430 ///
1431 /// See also:
1432 ///
1433 /// * [inputFormatters], which are called before [onChanged]
1434 /// runs and can validate and change ("format") the input value.
1435 /// * [onEditingComplete], [onSubmitted], [onSelectionChanged]:
1436 /// which are more specialized input change notifications.
1437 /// * [TextEditingController], which implements the [Listenable] interface
1438 /// and notifies its listeners on [TextEditingValue] changes.
1439 final ValueChanged<String>? onChanged;
1440
1441 /// {@template flutter.widgets.editableText.onEditingComplete}
1442 /// Called when the user submits editable content (e.g., user presses the "done"
1443 /// button on the keyboard).
1444 ///
1445 /// The default implementation of [onEditingComplete] executes 2 different
1446 /// behaviors based on the situation:
1447 ///
1448 /// - When a completion action is pressed, such as "done", "go", "send", or
1449 /// "search", the user's content is submitted to the [controller] and then
1450 /// focus is given up.
1451 ///
1452 /// - When a non-completion action is pressed, such as "next" or "previous",
1453 /// the user's content is submitted to the [controller], but focus is not
1454 /// given up because developers may want to immediately move focus to
1455 /// another input widget within [onSubmitted].
1456 ///
1457 /// Providing [onEditingComplete] prevents the aforementioned default behavior.
1458 /// {@endtemplate}
1459 final VoidCallback? onEditingComplete;
1460
1461 /// {@template flutter.widgets.editableText.onSubmitted}
1462 /// Called when the user indicates that they are done editing the text in the
1463 /// field.
1464 ///
1465 /// By default, [onSubmitted] is called after [onChanged] when the user
1466 /// has finalized editing; or, if the default behavior has been overridden,
1467 /// after [onEditingComplete]. See [onEditingComplete] for details.
1468 ///
1469 /// ## Testing
1470 /// The following is the recommended way to trigger [onSubmitted] in a test:
1471 ///
1472 /// ```dart
1473 /// await tester.testTextInput.receiveAction(TextInputAction.done);
1474 /// ```
1475 ///
1476 /// Sending a `LogicalKeyboardKey.enter` via `tester.sendKeyEvent` will not
1477 /// trigger [onSubmitted]. This is because on a real device, the engine
1478 /// translates the enter key to a done action, but `tester.sendKeyEvent` sends
1479 /// the key to the framework only.
1480 /// {@endtemplate}
1481 final ValueChanged<String>? onSubmitted;
1482
1483 /// {@template flutter.widgets.editableText.onAppPrivateCommand}
1484 /// This is used to receive a private command from the input method.
1485 ///
1486 /// Called when the result of [TextInputClient.performPrivateCommand] is
1487 /// received.
1488 ///
1489 /// This can be used to provide domain-specific features that are only known
1490 /// between certain input methods and their clients.
1491 ///
1492 /// See also:
1493 /// * [performPrivateCommand](https://developer.android.com/reference/android/view/inputmethod/InputConnection#performPrivateCommand\(java.lang.String,%20android.os.Bundle\)),
1494 /// which is the Android documentation for performPrivateCommand, used to
1495 /// send a command from the input method.
1496 /// * [sendAppPrivateCommand](https://developer.android.com/reference/android/view/inputmethod/InputMethodManager#sendAppPrivateCommand),
1497 /// which is the Android documentation for sendAppPrivateCommand, used to
1498 /// send a command to the input method.
1499 /// {@endtemplate}
1500 final AppPrivateCommandCallback? onAppPrivateCommand;
1501
1502 /// {@template flutter.widgets.editableText.onSelectionChanged}
1503 /// Called when the user changes the selection of text (including the cursor
1504 /// location).
1505 /// {@endtemplate}
1506 final SelectionChangedCallback? onSelectionChanged;
1507
1508 /// {@macro flutter.widgets.SelectionOverlay.onSelectionHandleTapped}
1509 final VoidCallback? onSelectionHandleTapped;
1510
1511 /// {@template flutter.widgets.editableText.groupId}
1512 /// The group identifier for the [TextFieldTapRegion] of this text field.
1513 ///
1514 /// Text fields with the same group identifier share the same tap region.
1515 /// Defaults to the type of [EditableText].
1516 ///
1517 /// See also:
1518 ///
1519 /// * [TextFieldTapRegion], to give a [groupId] to a widget that is to be
1520 /// included in a [EditableText]'s tap region that has [groupId] set.
1521 /// {@endtemplate}
1522 final Object groupId;
1523
1524 /// {@template flutter.widgets.editableText.onTapOutside}
1525 /// Called for each tap down that occurs outside of the [TextFieldTapRegion]
1526 /// group when the text field is focused.
1527 ///
1528 /// If this is null, [EditableTextTapOutsideIntent] will be invoked. In the
1529 /// default implementation, [FocusNode.unfocus] will be called on the
1530 /// [focusNode] for this text field when a [PointerDownEvent] is received on
1531 /// another part of the UI. However, it will not unfocus as a result of mobile
1532 /// application touch events (which does not include mouse clicks), to conform
1533 /// with the platform conventions. To change this behavior, a callback may be
1534 /// set here or [EditableTextTapOutsideIntent] may be overridden.
1535 ///
1536 /// When adding additional controls to a text field (for example, a spinner, a
1537 /// button that copies the selected text, or modifies formatting), it is
1538 /// helpful if tapping on that control doesn't unfocus the text field. In
1539 /// order for an external widget to be considered as part of the text field
1540 /// for the purposes of tapping "outside" of the field, wrap the control in a
1541 /// [TextFieldTapRegion].
1542 ///
1543 /// The [PointerDownEvent] passed to the function is the event that caused the
1544 /// notification. It is possible that the event may occur outside of the
1545 /// immediate bounding box defined by the text field, although it will be
1546 /// within the bounding box of a [TextFieldTapRegion] member.
1547 /// {@endtemplate}
1548 ///
1549 /// {@tool dartpad}
1550 /// This example shows how to use a `TextFieldTapRegion` to wrap a set of
1551 /// "spinner" buttons that increment and decrement a value in the [TextField]
1552 /// without causing the text field to lose keyboard focus.
1553 ///
1554 /// This example includes a generic `SpinnerField<T>` class that you can copy
1555 /// into your own project and customize.
1556 ///
1557 /// ** See code in examples/api/lib/widgets/tap_region/text_field_tap_region.0.dart **
1558 /// {@end-tool}
1559 ///
1560 /// See also:
1561 ///
1562 /// * [TapRegion] for how the region group is determined.
1563 /// * [onTapUpOutside] which is called for each tap up.
1564 /// * [EditableTextTapOutsideIntent] for the intent that is invoked if
1565 /// this is null.
1566 final TapRegionCallback? onTapOutside;
1567
1568 /// {@template flutter.widgets.editableText.onTapUpOutside}
1569 /// Called for each tap up that occurs outside of the [TextFieldTapRegion]
1570 /// group when the text field is focused.
1571 ///
1572 /// If this is null, [EditableTextTapUpOutsideIntent] will be invoked. In the
1573 /// default implementation, this is a no-op. To change this behavior, set a
1574 /// callback here or override [EditableTextTapUpOutsideIntent].
1575 ///
1576 /// The [PointerUpEvent] passed to the function is the event that caused the
1577 /// notification. It is possible that the event may occur outside of the
1578 /// immediate bounding box defined by the text field, although it will be
1579 /// within the bounding box of a [TextFieldTapRegion] member.
1580 /// {@endtemplate}
1581 ///
1582 /// See also:
1583 ///
1584 /// * [TapRegion] for how the region group is determined.
1585 /// * [onTapOutside], which is called for each tap down.
1586 /// * [EditableTextTapOutsideIntent], the intent that is invoked if
1587 /// this is null.
1588 final TapRegionUpCallback? onTapUpOutside;
1589
1590 /// {@template flutter.widgets.editableText.inputFormatters}
1591 /// Optional input validation and formatting overrides.
1592 ///
1593 /// Formatters are run in the provided order when the user changes the text
1594 /// this widget contains. When this parameter changes, the new formatters will
1595 /// not be applied until the next time the user inserts or deletes text.
1596 /// Similar to the [onChanged] callback, formatters don't run when the text is
1597 /// changed programmatically via [controller].
1598 ///
1599 /// See also:
1600 ///
1601 /// * [TextEditingController], which implements the [Listenable] interface
1602 /// and notifies its listeners on [TextEditingValue] changes.
1603 /// {@endtemplate}
1604 final List<TextInputFormatter>? inputFormatters;
1605
1606 /// The cursor for a mouse pointer when it enters or is hovering over the
1607 /// widget.
1608 ///
1609 /// If this property is null, [SystemMouseCursors.text] will be used.
1610 ///
1611 /// The [mouseCursor] is the only property of [EditableText] that controls the
1612 /// appearance of the mouse pointer. All other properties related to "cursor"
1613 /// stands for the text cursor, which is usually a blinking vertical line at
1614 /// the editing position.
1615 final MouseCursor? mouseCursor;
1616
1617 /// Whether the caller will provide gesture handling (true), or if the
1618 /// [EditableText] is expected to handle basic gestures (false).
1619 ///
1620 /// When this is false, the [EditableText] (or more specifically, the
1621 /// [RenderEditable]) enables some rudimentary gestures (tap to position the
1622 /// cursor, long-press to select all, and some scrolling behavior).
1623 ///
1624 /// These behaviors are sufficient for debugging purposes but are inadequate
1625 /// for user-facing applications. To enable platform-specific behaviors, use a
1626 /// [TextSelectionGestureDetectorBuilder] to wrap the [EditableText], and set
1627 /// [rendererIgnoresPointer] to true.
1628 ///
1629 /// When [rendererIgnoresPointer] is true true, the [RenderEditable] created
1630 /// by this widget will not handle pointer events.
1631 ///
1632 /// This property is false by default.
1633 ///
1634 /// See also:
1635 ///
1636 /// * [RenderEditable.ignorePointer], which implements this feature.
1637 /// * [TextSelectionGestureDetectorBuilder], which implements platform-specific
1638 /// gestures and behaviors.
1639 final bool rendererIgnoresPointer;
1640
1641 /// {@template flutter.widgets.editableText.cursorWidth}
1642 /// How thick the cursor will be.
1643 ///
1644 /// Defaults to 2.0.
1645 ///
1646 /// The cursor will draw under the text. The cursor width will extend
1647 /// to the right of the boundary between characters for left-to-right text
1648 /// and to the left for right-to-left text. This corresponds to extending
1649 /// downstream relative to the selected position. Negative values may be used
1650 /// to reverse this behavior.
1651 /// {@endtemplate}
1652 final double cursorWidth;
1653
1654 /// {@template flutter.widgets.editableText.cursorHeight}
1655 /// How tall the cursor will be.
1656 ///
1657 /// If this property is null, [RenderEditable.preferredLineHeight] will be used.
1658 /// {@endtemplate}
1659 final double? cursorHeight;
1660
1661 /// {@template flutter.widgets.editableText.cursorRadius}
1662 /// How rounded the corners of the cursor should be.
1663 ///
1664 /// By default, the cursor has no radius.
1665 /// {@endtemplate}
1666 final Radius? cursorRadius;
1667
1668 /// {@template flutter.widgets.editableText.cursorOpacityAnimates}
1669 /// Whether the cursor will animate from fully transparent to fully opaque
1670 /// during each cursor blink.
1671 ///
1672 /// By default, the cursor opacity will animate on iOS platforms and will not
1673 /// animate on Android platforms.
1674 /// {@endtemplate}
1675 final bool cursorOpacityAnimates;
1676
1677 /// {@macro flutter.rendering.RenderEditable.cursorOffset}
1678 final Offset? cursorOffset;
1679
1680 /// {@macro flutter.rendering.RenderEditable.paintCursorAboveText}
1681 final bool paintCursorAboveText;
1682
1683 /// Controls how tall the selection highlight boxes are computed to be.
1684 ///
1685 /// See [ui.BoxHeightStyle] for details on available styles.
1686 final ui.BoxHeightStyle selectionHeightStyle;
1687
1688 /// Controls how wide the selection highlight boxes are computed to be.
1689 ///
1690 /// See [ui.BoxWidthStyle] for details on available styles.
1691 final ui.BoxWidthStyle selectionWidthStyle;
1692
1693 /// The appearance of the keyboard.
1694 ///
1695 /// This setting is only honored on iOS devices.
1696 ///
1697 /// Defaults to [Brightness.light].
1698 final Brightness keyboardAppearance;
1699
1700 /// {@template flutter.widgets.editableText.scrollPadding}
1701 /// Configures the padding for the edges surrounding a [Scrollable] when the
1702 /// text field scrolls into view.
1703 ///
1704 /// When this widget receives focus and is not completely visible (for example
1705 /// scrolled partially off the screen or overlapped by the keyboard), then it
1706 /// will attempt to make itself visible by scrolling a surrounding
1707 /// [Scrollable], if one is present. This value controls how far from the
1708 /// edges of a [Scrollable] the TextField will be positioned after the scroll.
1709 ///
1710 /// Defaults to EdgeInsets.all(20.0).
1711 /// {@endtemplate}
1712 final EdgeInsets scrollPadding;
1713
1714 /// {@template flutter.widgets.editableText.enableInteractiveSelection}
1715 /// Whether to enable user interface affordances for changing the
1716 /// text selection.
1717 ///
1718 /// For example, setting this to true will enable features such as
1719 /// long-pressing the TextField to select text and show the
1720 /// cut/copy/paste menu, and tapping to move the text caret.
1721 ///
1722 /// When this is false, the text selection cannot be adjusted by
1723 /// the user, the cut/copy/paste menu is hidden, and the shortcuts to
1724 /// cut/copy/paste text do nothing but stop propagation of the key event
1725 /// to other key event handlers in the focus chain.
1726 ///
1727 /// Defaults to true.
1728 /// {@endtemplate}
1729 final bool enableInteractiveSelection;
1730
1731 /// Setting this property to true makes the cursor stop blinking or fading
1732 /// on and off once the cursor appears on focus. This property is useful for
1733 /// testing purposes.
1734 ///
1735 /// It does not affect the necessity to focus the EditableText for the cursor
1736 /// to appear in the first place.
1737 ///
1738 /// Defaults to false, resulting in a typical blinking cursor.
1739 static bool debugDeterministicCursor = false;
1740
1741 /// {@macro flutter.widgets.scrollable.dragStartBehavior}
1742 final DragStartBehavior dragStartBehavior;
1743
1744 /// {@template flutter.widgets.editableText.scrollController}
1745 /// The [ScrollController] to use when vertically scrolling the input.
1746 ///
1747 /// If null, it will instantiate a new ScrollController.
1748 ///
1749 /// See [Scrollable.controller].
1750 /// {@endtemplate}
1751 final ScrollController? scrollController;
1752
1753 /// {@template flutter.widgets.editableText.scrollPhysics}
1754 /// The [ScrollPhysics] to use when vertically scrolling the input.
1755 ///
1756 /// If not specified, it will behave according to the current platform.
1757 ///
1758 /// See [Scrollable.physics].
1759 /// {@endtemplate}
1760 ///
1761 /// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the
1762 /// [ScrollPhysics] provided by that behavior will take precedence after
1763 /// [scrollPhysics].
1764 final ScrollPhysics? scrollPhysics;
1765
1766 /// {@template flutter.widgets.editableText.scribbleEnabled}
1767 /// Whether iOS 14 Scribble features are enabled for this widget.
1768 ///
1769 /// Only available on iPads.
1770 ///
1771 /// Defaults to true.
1772 /// {@endtemplate}
1773 @Deprecated(
1774 'Use `stylusHandwritingEnabled` instead. '
1775 'This feature was deprecated after v3.27.0-0.2.pre.',
1776 )
1777 final bool scribbleEnabled;
1778
1779 /// {@template flutter.widgets.editableText.stylusHandwritingEnabled}
1780 /// Whether this input supports stylus handwriting, where the user can write
1781 /// directly on top of a field.
1782 ///
1783 /// Currently only the following devices are supported:
1784 ///
1785 /// * iPads running iOS 14 and above using an Apple Pencil.
1786 /// * Android devices running API 34 and above and using an active stylus.
1787 /// {@endtemplate}
1788 ///
1789 /// On Android, Scribe gestures are detected outside of [EditableText],
1790 /// typically by [TextSelectionGestureDetectorBuilder]. This is handled
1791 /// automatically in [TextField].
1792 ///
1793 /// See also:
1794 ///
1795 /// * [ScribbleClient], which can be mixed into an arbitrary widget to
1796 /// provide iOS Scribble functionality.
1797 /// * [Scribe], which can be used to interact with Android Scribe directly.
1798 final bool stylusHandwritingEnabled;
1799
1800 /// {@template flutter.widgets.editableText.selectionEnabled}
1801 /// Same as [enableInteractiveSelection].
1802 ///
1803 /// This getter exists primarily for consistency with
1804 /// [RenderEditable.selectionEnabled].
1805 /// {@endtemplate}
1806 bool get selectionEnabled => enableInteractiveSelection;
1807
1808 /// {@template flutter.widgets.editableText.selectAllOnFocus}
1809 /// Whether this field should select all text when gaining focus.
1810 ///
1811 /// When false, focusing this text field will leave its
1812 /// existing text selection unchanged.
1813 ///
1814 /// Defaults to true on web and desktop platforms, and false on mobile platforms.
1815 /// {@endtemplate}
1816 final bool selectAllOnFocus;
1817
1818 /// {@template flutter.widgets.editableText.autofillHints}
1819 /// A list of strings that helps the autofill service identify the type of this
1820 /// text input.
1821 ///
1822 /// When set to null, this text input will not send its autofill information
1823 /// to the platform, preventing it from participating in autofills triggered
1824 /// by a different [AutofillClient], even if they're in the same
1825 /// [AutofillScope]. Additionally, on Android and web, setting this to null
1826 /// will disable autofill for this text field.
1827 ///
1828 /// The minimum platform SDK version that supports Autofill is API level 26
1829 /// for Android, and iOS 10.0 for iOS.
1830 ///
1831 /// Defaults to an empty list.
1832 ///
1833 /// ### Setting up iOS autofill:
1834 ///
1835 /// To provide the best user experience and ensure your app fully supports
1836 /// password autofill on iOS, follow these steps:
1837 ///
1838 /// * Set up your iOS app's
1839 /// [associated domains](https://developer.apple.com/documentation/safariservices/supporting_associated_domains_in_your_app).
1840 /// * Some autofill hints only work with specific [keyboardType]s. For example,
1841 /// [AutofillHints.name] requires [TextInputType.name] and [AutofillHints.email]
1842 /// works only with [TextInputType.emailAddress]. Make sure the input field has a
1843 /// compatible [keyboardType]. Empirically, [TextInputType.name] works well
1844 /// with many autofill hints that are predefined on iOS.
1845 ///
1846 /// ### Troubleshooting Autofill
1847 ///
1848 /// Autofill service providers rely heavily on [autofillHints]. Make sure the
1849 /// entries in [autofillHints] are supported by the autofill service currently
1850 /// in use (the name of the service can typically be found in your mobile
1851 /// device's system settings).
1852 ///
1853 /// #### Autofill UI refuses to show up when I tap on the text field
1854 ///
1855 /// Check the device's system settings and make sure autofill is turned on,
1856 /// and there are available credentials stored in the autofill service.
1857 ///
1858 /// * iOS password autofill: Go to Settings -> Password, turn on "Autofill
1859 /// Passwords", and add new passwords for testing by pressing the top right
1860 /// "+" button. Use an arbitrary "website" if you don't have associated
1861 /// domains set up for your app. As long as there's at least one password
1862 /// stored, you should be able to see a key-shaped icon in the quick type
1863 /// bar on the software keyboard, when a password related field is focused.
1864 ///
1865 /// * iOS contact information autofill: iOS seems to pull contact info from
1866 /// the Apple ID currently associated with the device. Go to Settings ->
1867 /// Apple ID (usually the first entry, or "Sign in to your iPhone" if you
1868 /// haven't set up one on the device), and fill out the relevant fields. If
1869 /// you wish to test more contact info types, try adding them in Contacts ->
1870 /// My Card.
1871 ///
1872 /// * Android autofill: Go to Settings -> System -> Languages & input ->
1873 /// Autofill service. Enable the autofill service of your choice, and make
1874 /// sure there are available credentials associated with your app.
1875 ///
1876 /// Specifying [InputDecoration.hintText] may also help autofill services
1877 /// (like Samsung Pass) determine the expected content type of an input field,
1878 /// although this is typically not required when autofillHints are present.
1879 ///
1880 /// #### I called `TextInput.finishAutofillContext` but the autofill save
1881 /// prompt isn't showing
1882 ///
1883 /// * iOS: iOS may not show a prompt or any other visual indication when it
1884 /// saves user password. Go to Settings -> Password and check if your new
1885 /// password is saved. Neither saving password nor auto-generating strong
1886 /// password works without properly setting up associated domains in your
1887 /// app. To set up associated domains, follow the instructions in
1888 /// <https://developer.apple.com/documentation/safariservices/supporting_associated_domains_in_your_app>.
1889 ///
1890 /// {@endtemplate}
1891 /// {@macro flutter.services.AutofillConfiguration.autofillHints}
1892 final Iterable<String>? autofillHints;
1893
1894 /// The [AutofillClient] that controls this input field's autofill behavior.
1895 ///
1896 /// When null, this widget's [EditableTextState] will be used as the
1897 /// [AutofillClient]. This property may override [autofillHints].
1898 final AutofillClient? autofillClient;
1899
1900 /// {@macro flutter.material.Material.clipBehavior}
1901 ///
1902 /// Defaults to [Clip.hardEdge].
1903 final Clip clipBehavior;
1904
1905 /// Restoration ID to save and restore the scroll offset of the
1906 /// [EditableText].
1907 ///
1908 /// If a restoration id is provided, the [EditableText] will persist its
1909 /// current scroll offset and restore it during state restoration.
1910 ///
1911 /// The scroll offset is persisted in a [RestorationBucket] claimed from
1912 /// the surrounding [RestorationScope] using the provided restoration ID.
1913 ///
1914 /// Persisting and restoring the content of the [EditableText] is the
1915 /// responsibility of the owner of the [controller], who may use a
1916 /// [RestorableTextEditingController] for that purpose.
1917 ///
1918 /// See also:
1919 ///
1920 /// * [RestorationManager], which explains how state restoration works in
1921 /// Flutter.
1922 final String? restorationId;
1923
1924 /// {@template flutter.widgets.editableText.scrollBehavior}
1925 /// A [ScrollBehavior] that will be applied to this widget individually.
1926 ///
1927 /// Defaults to null, wherein the inherited [ScrollBehavior] is copied and
1928 /// modified to alter the viewport decoration, like [Scrollbar]s.
1929 ///
1930 /// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit
1931 /// [ScrollPhysics] is provided in [scrollPhysics], it will take precedence,
1932 /// followed by [scrollBehavior], and then the inherited ancestor
1933 /// [ScrollBehavior].
1934 /// {@endtemplate}
1935 ///
1936 /// The [ScrollBehavior] of the inherited [ScrollConfiguration] will be
1937 /// modified by default to only apply a [Scrollbar] if [maxLines] is greater
1938 /// than 1.
1939 final ScrollBehavior? scrollBehavior;
1940
1941 /// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
1942 final bool enableIMEPersonalizedLearning;
1943
1944 /// {@template flutter.widgets.editableText.contentInsertionConfiguration}
1945 /// Configuration of handler for media content inserted via the system input
1946 /// method.
1947 ///
1948 /// Defaults to null in which case media content insertion will be disabled,
1949 /// and the system will display a message informing the user that the text field
1950 /// does not support inserting media content.
1951 ///
1952 /// Set [ContentInsertionConfiguration.onContentInserted] to provide a handler.
1953 /// Additionally, set [ContentInsertionConfiguration.allowedMimeTypes]
1954 /// to limit the allowable mime types for inserted content.
1955 ///
1956 /// {@tool dartpad}
1957 ///
1958 /// This example shows how to access the data for inserted content in your
1959 /// `TextField`.
1960 ///
1961 /// ** See code in examples/api/lib/widgets/editable_text/editable_text.on_content_inserted.0.dart **
1962 /// {@end-tool}
1963 ///
1964 /// If [contentInsertionConfiguration] is not provided, by default
1965 /// an empty list of mime types will be sent to the Flutter Engine.
1966 /// A handler function must be provided in order to customize the allowable
1967 /// mime types for inserted content.
1968 ///
1969 /// If rich content is inserted without a handler, the system will display
1970 /// a message informing the user that the current text input does not support
1971 /// inserting rich content.
1972 /// {@endtemplate}
1973 final ContentInsertionConfiguration? contentInsertionConfiguration;
1974
1975 /// {@template flutter.widgets.EditableText.contextMenuBuilder}
1976 /// Builds the text selection toolbar when requested by the user.
1977 ///
1978 /// The context menu is built when [EditableTextState.showToolbar] is called,
1979 /// typically by one of the callbacks installed by the widget created by
1980 /// [TextSelectionGestureDetectorBuilder.buildGestureDetector]. The widget
1981 /// returned by [contextMenuBuilder] is passed to a [ContextMenuController].
1982 ///
1983 /// If no callback is provided, no context menu will be shown.
1984 ///
1985 /// The [EditableTextContextMenuBuilder] signature used by the
1986 /// [contextMenuBuilder] callback has two parameters, the [BuildContext] of
1987 /// the [EditableText] and the [EditableTextState] of the [EditableText].
1988 ///
1989 /// The [EditableTextState] has two properties that are especially useful when
1990 /// building the widgets for the context menu:
1991 ///
1992 /// * [EditableTextState.contextMenuAnchors] specifies the desired anchor
1993 /// position for the context menu.
1994 ///
1995 /// * [EditableTextState.contextMenuButtonItems] represents the buttons that
1996 /// should typically be built for this widget (e.g. cut, copy, paste).
1997 ///
1998 /// The [TextSelectionToolbarLayoutDelegate] class may be particularly useful
1999 /// in honoring the preferred anchor positions.
2000 ///
2001 /// For backwards compatibility, when [EditableText.selectionControls] is set
2002 /// to an object that does not mix in [TextSelectionHandleControls],
2003 /// [contextMenuBuilder] is ignored and the
2004 /// [TextSelectionControls.buildToolbar] method is used instead.
2005 ///
2006 /// {@tool dartpad}
2007 /// This example shows how to customize the menu, in this case by keeping the
2008 /// default buttons for the platform but modifying their appearance.
2009 ///
2010 /// ** See code in examples/api/lib/material/context_menu/editable_text_toolbar_builder.0.dart **
2011 /// {@end-tool}
2012 ///
2013 /// {@tool dartpad}
2014 /// This example shows how to show a custom button only when an email address
2015 /// is currently selected.
2016 ///
2017 /// ** See code in examples/api/lib/material/context_menu/editable_text_toolbar_builder.1.dart **
2018 /// {@end-tool}
2019 ///
2020 /// See also:
2021 /// * [AdaptiveTextSelectionToolbar], which builds the default text selection
2022 /// toolbar for the current platform, but allows customization of the
2023 /// buttons.
2024 /// * [AdaptiveTextSelectionToolbar.getAdaptiveButtons], which builds the
2025 /// button Widgets for the current platform given
2026 /// [ContextMenuButtonItem]s.
2027 /// * [BrowserContextMenu], which allows the browser's context menu on web
2028 /// to be disabled and Flutter-rendered context menus to appear.
2029 /// {@endtemplate}
2030 final EditableTextContextMenuBuilder? contextMenuBuilder;
2031
2032 /// {@template flutter.widgets.EditableText.spellCheckConfiguration}
2033 /// Configuration that details how spell check should be performed.
2034 ///
2035 /// Specifies the [SpellCheckService] used to spell check text input and the
2036 /// [TextStyle] used to style text with misspelled words.
2037 ///
2038 /// If the [SpellCheckService] is left null, spell check is disabled by
2039 /// default unless the [DefaultSpellCheckService] is supported, in which case
2040 /// it is used. It is currently supported only on Android and iOS.
2041 ///
2042 /// If this configuration is left null, then spell check is disabled by default.
2043 /// {@endtemplate}
2044 final SpellCheckConfiguration? spellCheckConfiguration;
2045
2046 /// The configuration for the magnifier to use with selections in this text
2047 /// field.
2048 ///
2049 /// {@macro flutter.widgets.magnifier.intro}
2050 final TextMagnifierConfiguration magnifierConfiguration;
2051
2052 /// {@macro flutter.services.TextInputConfiguration.hintLocales}
2053 final List<Locale>? hintLocales;
2054
2055 /// The default value for [selectionHeightStyle].
2056 ///
2057 /// On web platforms, this defaults to [ui.BoxHeightStyle.max].
2058 ///
2059 /// On native platforms, this defaults to [ui.BoxHeightStyle.includeLineSpacingMiddle] for all
2060 /// platforms.
2061 static ui.BoxHeightStyle get defaultSelectionHeightStyle {
2062 if (kIsWeb) {
2063 return ui.BoxHeightStyle.max;
2064 }
2065 return ui.BoxHeightStyle.includeLineSpacingMiddle;
2066 }
2067
2068 /// The default value for [selectionWidthStyle].
2069 ///
2070 /// On web platforms, this defaults to [ui.BoxWidthStyle.max] for Apple platforms running
2071 /// Safari (webkit) based browsers and [ui.BoxWidthStyle.tight] for all others.
2072 ///
2073 /// On non-web platforms, this defaults to [ui.BoxWidthStyle.max].
2074 static ui.BoxWidthStyle get defaultSelectionWidthStyle {
2075 if (kIsWeb) {
2076 if (defaultTargetPlatform == TargetPlatform.iOS || WebBrowserDetection.isSafari) {
2077 // On macOS web, the selection width behavior differs when running on
2078 // Chrom(e|ium) (blink) or Safari (webkit).
2079 return ui.BoxWidthStyle.max;
2080 }
2081 return ui.BoxWidthStyle.tight;
2082 }
2083 return ui.BoxWidthStyle.max;
2084 }
2085
2086 /// The default value for [stylusHandwritingEnabled].
2087 static const bool defaultStylusHandwritingEnabled = true;
2088
2089 bool get _userSelectionEnabled => enableInteractiveSelection && (!readOnly || !obscureText);
2090
2091 /// The default value for [selectAllOnFocus].
2092 static bool get _defaultSelectAllOnFocus {
2093 if (kIsWeb) {
2094 return true;
2095 }
2096 return switch (defaultTargetPlatform) {
2097 TargetPlatform.android => false,
2098 TargetPlatform.iOS => false,
2099 TargetPlatform.fuchsia => false,
2100 TargetPlatform.linux => true,
2101 TargetPlatform.macOS => true,
2102 TargetPlatform.windows => true,
2103 };
2104 }
2105
2106 /// Returns the [ContextMenuButtonItem]s representing the buttons in this
2107 /// platform's default selection menu for an editable field.
2108 ///
2109 /// For example, [EditableText] uses this to generate the default buttons for
2110 /// its context menu.
2111 ///
2112 /// See also:
2113 ///
2114 /// * [EditableTextState.contextMenuButtonItems], which gives the
2115 /// [ContextMenuButtonItem]s for a specific EditableText.
2116 /// * [SelectableRegion.getSelectableButtonItems], which performs a similar
2117 /// role but for content that is selectable but not editable.
2118 /// * [AdaptiveTextSelectionToolbar], which builds the toolbar itself, and can
2119 /// take a list of [ContextMenuButtonItem]s with
2120 /// [AdaptiveTextSelectionToolbar.buttonItems].
2121 /// * [AdaptiveTextSelectionToolbar.getAdaptiveButtons], which builds the button
2122 /// Widgets for the current platform given [ContextMenuButtonItem]s.
2123 static List<ContextMenuButtonItem> getEditableButtonItems({
2124 required final ClipboardStatus? clipboardStatus,
2125 required final VoidCallback? onCopy,
2126 required final VoidCallback? onCut,
2127 required final VoidCallback? onPaste,
2128 required final VoidCallback? onSelectAll,
2129 required final VoidCallback? onLookUp,
2130 required final VoidCallback? onSearchWeb,
2131 required final VoidCallback? onShare,
2132 required final VoidCallback? onLiveTextInput,
2133 }) {
2134 final List<ContextMenuButtonItem> resultButtonItem = <ContextMenuButtonItem>[];
2135
2136 // Configure button items with clipboard.
2137 if (onPaste == null || clipboardStatus != ClipboardStatus.unknown) {
2138 // If the paste button is enabled, don't render anything until the state
2139 // of the clipboard is known, since it's used to determine if paste is
2140 // shown.
2141
2142 // On Android, the share button is before the select all button.
2143 final bool showShareBeforeSelectAll = defaultTargetPlatform == TargetPlatform.android;
2144
2145 resultButtonItem.addAll(<ContextMenuButtonItem>[
2146 if (onCut != null) ContextMenuButtonItem(onPressed: onCut, type: ContextMenuButtonType.cut),
2147 if (onCopy != null)
2148 ContextMenuButtonItem(onPressed: onCopy, type: ContextMenuButtonType.copy),
2149 if (onPaste != null)
2150 ContextMenuButtonItem(onPressed: onPaste, type: ContextMenuButtonType.paste),
2151 if (onShare != null && showShareBeforeSelectAll)
2152 ContextMenuButtonItem(onPressed: onShare, type: ContextMenuButtonType.share),
2153 if (onSelectAll != null)
2154 ContextMenuButtonItem(onPressed: onSelectAll, type: ContextMenuButtonType.selectAll),
2155 if (onLookUp != null)
2156 ContextMenuButtonItem(onPressed: onLookUp, type: ContextMenuButtonType.lookUp),
2157 if (onSearchWeb != null)
2158 ContextMenuButtonItem(onPressed: onSearchWeb, type: ContextMenuButtonType.searchWeb),
2159 if (onShare != null && !showShareBeforeSelectAll)
2160 ContextMenuButtonItem(onPressed: onShare, type: ContextMenuButtonType.share),
2161 ]);
2162 }
2163
2164 // Config button items with Live Text.
2165 if (onLiveTextInput != null) {
2166 resultButtonItem.add(
2167 ContextMenuButtonItem(
2168 onPressed: onLiveTextInput,
2169 type: ContextMenuButtonType.liveTextInput,
2170 ),
2171 );
2172 }
2173
2174 return resultButtonItem;
2175 }
2176
2177 // Infer the value of autocorrect from autofillHints.
2178 static bool _inferAutocorrect({required Iterable<String>? autofillHints}) {
2179 if (autofillHints == null || autofillHints.isEmpty || kIsWeb) {
2180 return true;
2181 }
2182
2183 switch (defaultTargetPlatform) {
2184 case TargetPlatform.iOS:
2185 // username, password and newPassword are password related hint.
2186 // newUsername is not supported on iOS.
2187 final bool passwordRelatedHint = autofillHints.any(
2188 (String hint) =>
2189 hint == AutofillHints.username ||
2190 hint == AutofillHints.password ||
2191 hint == AutofillHints.newPassword,
2192 );
2193 if (passwordRelatedHint) {
2194 // https://github.com/flutter/flutter/issues/134723
2195 // Set autocorrect to false to prevent password bar from flashing.
2196 return false;
2197 }
2198 case TargetPlatform.macOS:
2199 case TargetPlatform.android:
2200 case TargetPlatform.fuchsia:
2201 case TargetPlatform.linux:
2202 case TargetPlatform.windows:
2203 break;
2204 }
2205
2206 return true;
2207 }
2208
2209 // Infer the keyboard type of an `EditableText` if it's not specified.
2210 static TextInputType _inferKeyboardType({
2211 required Iterable<String>? autofillHints,
2212 required int? maxLines,
2213 }) {
2214 if (autofillHints == null || autofillHints.isEmpty) {
2215 return maxLines == 1 ? TextInputType.text : TextInputType.multiline;
2216 }
2217
2218 final String effectiveHint = autofillHints.first;
2219
2220 // On iOS oftentimes specifying a text content type is not enough to qualify
2221 // the input field for autofill. The keyboard type also needs to be compatible
2222 // with the content type. To get autofill to work by default on EditableText,
2223 // the keyboard type inference on iOS is done differently from other platforms.
2224 //
2225 // The entries with "autofill not working" comments are the iOS text content
2226 // types that should work with the specified keyboard type but won't trigger
2227 // (even within a native app). Tested on iOS 13.5.
2228 if (!kIsWeb) {
2229 switch (defaultTargetPlatform) {
2230 case TargetPlatform.iOS:
2231 case TargetPlatform.macOS:
2232 const Map<String, TextInputType> iOSKeyboardType = <String, TextInputType>{
2233 AutofillHints.addressCity: TextInputType.name,
2234 AutofillHints.addressCityAndState: TextInputType.name, // Autofill not working.
2235 AutofillHints.addressState: TextInputType.name,
2236 AutofillHints.countryName: TextInputType.name,
2237 AutofillHints.creditCardNumber: TextInputType.number, // Couldn't test.
2238 AutofillHints.email: TextInputType.emailAddress,
2239 AutofillHints.familyName: TextInputType.name,
2240 AutofillHints.fullStreetAddress: TextInputType.name,
2241 AutofillHints.givenName: TextInputType.name,
2242 AutofillHints.jobTitle: TextInputType.name, // Autofill not working.
2243 AutofillHints.location: TextInputType.name, // Autofill not working.
2244 AutofillHints.middleName: TextInputType.name, // Autofill not working.
2245 AutofillHints.name: TextInputType.name,
2246 AutofillHints.namePrefix: TextInputType.name, // Autofill not working.
2247 AutofillHints.nameSuffix: TextInputType.name, // Autofill not working.
2248 AutofillHints.newPassword: TextInputType.text,
2249 AutofillHints.newUsername: TextInputType.text,
2250 AutofillHints.nickname: TextInputType.name, // Autofill not working.
2251 AutofillHints.oneTimeCode: TextInputType.number,
2252 AutofillHints.organizationName: TextInputType.text, // Autofill not working.
2253 AutofillHints.password: TextInputType.text,
2254 AutofillHints.postalCode: TextInputType.name,
2255 AutofillHints.streetAddressLine1: TextInputType.name,
2256 AutofillHints.streetAddressLine2: TextInputType.name, // Autofill not working.
2257 AutofillHints.sublocality: TextInputType.name, // Autofill not working.
2258 AutofillHints.telephoneNumber: TextInputType.name,
2259 AutofillHints.url: TextInputType.url, // Autofill not working.
2260 AutofillHints.username: TextInputType.text,
2261 };
2262
2263 final TextInputType? keyboardType = iOSKeyboardType[effectiveHint];
2264 if (keyboardType != null) {
2265 return keyboardType;
2266 }
2267 case TargetPlatform.android:
2268 case TargetPlatform.fuchsia:
2269 case TargetPlatform.linux:
2270 case TargetPlatform.windows:
2271 break;
2272 }
2273 }
2274
2275 if (maxLines != 1) {
2276 return TextInputType.multiline;
2277 }
2278
2279 const Map<String, TextInputType> inferKeyboardType = <String, TextInputType>{
2280 AutofillHints.addressCity: TextInputType.streetAddress,
2281 AutofillHints.addressCityAndState: TextInputType.streetAddress,
2282 AutofillHints.addressState: TextInputType.streetAddress,
2283 AutofillHints.birthday: TextInputType.datetime,
2284 AutofillHints.birthdayDay: TextInputType.datetime,
2285 AutofillHints.birthdayMonth: TextInputType.datetime,
2286 AutofillHints.birthdayYear: TextInputType.datetime,
2287 AutofillHints.countryCode: TextInputType.number,
2288 AutofillHints.countryName: TextInputType.text,
2289 AutofillHints.creditCardExpirationDate: TextInputType.datetime,
2290 AutofillHints.creditCardExpirationDay: TextInputType.datetime,
2291 AutofillHints.creditCardExpirationMonth: TextInputType.datetime,
2292 AutofillHints.creditCardExpirationYear: TextInputType.datetime,
2293 AutofillHints.creditCardFamilyName: TextInputType.name,
2294 AutofillHints.creditCardGivenName: TextInputType.name,
2295 AutofillHints.creditCardMiddleName: TextInputType.name,
2296 AutofillHints.creditCardName: TextInputType.name,
2297 AutofillHints.creditCardNumber: TextInputType.number,
2298 AutofillHints.creditCardSecurityCode: TextInputType.number,
2299 AutofillHints.creditCardType: TextInputType.text,
2300 AutofillHints.email: TextInputType.emailAddress,
2301 AutofillHints.familyName: TextInputType.name,
2302 AutofillHints.fullStreetAddress: TextInputType.streetAddress,
2303 AutofillHints.gender: TextInputType.text,
2304 AutofillHints.givenName: TextInputType.name,
2305 AutofillHints.impp: TextInputType.url,
2306 AutofillHints.jobTitle: TextInputType.text,
2307 AutofillHints.language: TextInputType.text,
2308 AutofillHints.location: TextInputType.streetAddress,
2309 AutofillHints.middleInitial: TextInputType.name,
2310 AutofillHints.middleName: TextInputType.name,
2311 AutofillHints.name: TextInputType.name,
2312 AutofillHints.namePrefix: TextInputType.name,
2313 AutofillHints.nameSuffix: TextInputType.name,
2314 AutofillHints.newPassword: TextInputType.text,
2315 AutofillHints.newUsername: TextInputType.text,
2316 AutofillHints.nickname: TextInputType.text,
2317 AutofillHints.oneTimeCode: TextInputType.text,
2318 AutofillHints.organizationName: TextInputType.text,
2319 AutofillHints.password: TextInputType.text,
2320 AutofillHints.photo: TextInputType.text,
2321 AutofillHints.postalAddress: TextInputType.streetAddress,
2322 AutofillHints.postalAddressExtended: TextInputType.streetAddress,
2323 AutofillHints.postalAddressExtendedPostalCode: TextInputType.number,
2324 AutofillHints.postalCode: TextInputType.number,
2325 AutofillHints.streetAddressLevel1: TextInputType.streetAddress,
2326 AutofillHints.streetAddressLevel2: TextInputType.streetAddress,
2327 AutofillHints.streetAddressLevel3: TextInputType.streetAddress,
2328 AutofillHints.streetAddressLevel4: TextInputType.streetAddress,
2329 AutofillHints.streetAddressLine1: TextInputType.streetAddress,
2330 AutofillHints.streetAddressLine2: TextInputType.streetAddress,
2331 AutofillHints.streetAddressLine3: TextInputType.streetAddress,
2332 AutofillHints.sublocality: TextInputType.streetAddress,
2333 AutofillHints.telephoneNumber: TextInputType.phone,
2334 AutofillHints.telephoneNumberAreaCode: TextInputType.phone,
2335 AutofillHints.telephoneNumberCountryCode: TextInputType.phone,
2336 AutofillHints.telephoneNumberDevice: TextInputType.phone,
2337 AutofillHints.telephoneNumberExtension: TextInputType.phone,
2338 AutofillHints.telephoneNumberLocal: TextInputType.phone,
2339 AutofillHints.telephoneNumberLocalPrefix: TextInputType.phone,
2340 AutofillHints.telephoneNumberLocalSuffix: TextInputType.phone,
2341 AutofillHints.telephoneNumberNational: TextInputType.phone,
2342 AutofillHints.transactionAmount: TextInputType.numberWithOptions(decimal: true),
2343 AutofillHints.transactionCurrency: TextInputType.text,
2344 AutofillHints.url: TextInputType.url,
2345 AutofillHints.username: TextInputType.text,
2346 };
2347
2348 return inferKeyboardType[effectiveHint] ?? TextInputType.text;
2349 }
2350
2351 @override
2352 EditableTextState createState() => EditableTextState();
2353
2354 @override
2355 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
2356 super.debugFillProperties(properties);
2357 properties.add(DiagnosticsProperty<TextEditingController>('controller', controller));
2358 properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode));
2359 properties.add(DiagnosticsProperty<bool>('obscureText', obscureText, defaultValue: false));
2360 properties.add(DiagnosticsProperty<bool>('readOnly', readOnly, defaultValue: false));
2361 properties.add(DiagnosticsProperty<bool>('autocorrect', autocorrect, defaultValue: null));
2362 properties.add(
2363 EnumProperty<SmartDashesType>(
2364 'smartDashesType',
2365 smartDashesType,
2366 defaultValue: obscureText ? SmartDashesType.disabled : SmartDashesType.enabled,
2367 ),
2368 );
2369 properties.add(
2370 EnumProperty<SmartQuotesType>(
2371 'smartQuotesType',
2372 smartQuotesType,
2373 defaultValue: obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled,
2374 ),
2375 );
2376 properties.add(
2377 DiagnosticsProperty<bool>('enableSuggestions', enableSuggestions, defaultValue: true),
2378 );
2379 style.debugFillProperties(properties);
2380 properties.add(EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: null));
2381 properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
2382 properties.add(DiagnosticsProperty<Locale>('locale', locale, defaultValue: null));
2383 properties.add(DiagnosticsProperty<TextScaler>('textScaler', textScaler, defaultValue: null));
2384 properties.add(IntProperty('maxLines', maxLines, defaultValue: 1));
2385 properties.add(IntProperty('minLines', minLines, defaultValue: null));
2386 properties.add(DiagnosticsProperty<bool>('expands', expands, defaultValue: false));
2387 properties.add(DiagnosticsProperty<bool>('autofocus', autofocus, defaultValue: false));
2388 properties.add(
2389 DiagnosticsProperty<TextInputType>('keyboardType', keyboardType, defaultValue: null),
2390 );
2391 properties.add(
2392 DiagnosticsProperty<ScrollController>(
2393 'scrollController',
2394 scrollController,
2395 defaultValue: null,
2396 ),
2397 );
2398 properties.add(
2399 DiagnosticsProperty<ScrollPhysics>('scrollPhysics', scrollPhysics, defaultValue: null),
2400 );
2401 properties.add(
2402 DiagnosticsProperty<Iterable<String>>('autofillHints', autofillHints, defaultValue: null),
2403 );
2404 properties.add(
2405 DiagnosticsProperty<TextHeightBehavior>(
2406 'textHeightBehavior',
2407 textHeightBehavior,
2408 defaultValue: null,
2409 ),
2410 );
2411 properties.add(
2412 DiagnosticsProperty<bool>('scribbleEnabled', scribbleEnabled, defaultValue: true),
2413 );
2414 properties.add(
2415 DiagnosticsProperty<bool>(
2416 'stylusHandwritingEnabled',
2417 stylusHandwritingEnabled,
2418 defaultValue: defaultStylusHandwritingEnabled,
2419 ),
2420 );
2421 properties.add(
2422 DiagnosticsProperty<bool>(
2423 'enableIMEPersonalizedLearning',
2424 enableIMEPersonalizedLearning,
2425 defaultValue: true,
2426 ),
2427 );
2428 properties.add(
2429 DiagnosticsProperty<bool>(
2430 'enableInteractiveSelection',
2431 enableInteractiveSelection,
2432 defaultValue: true,
2433 ),
2434 );
2435 properties.add(
2436 DiagnosticsProperty<UndoHistoryController>(
2437 'undoController',
2438 undoController,
2439 defaultValue: null,
2440 ),
2441 );
2442 properties.add(
2443 DiagnosticsProperty<SpellCheckConfiguration>(
2444 'spellCheckConfiguration',
2445 spellCheckConfiguration,
2446 defaultValue: null,
2447 ),
2448 );
2449 properties.add(
2450 DiagnosticsProperty<List<String>>(
2451 'contentCommitMimeTypes',
2452 contentInsertionConfiguration?.allowedMimeTypes ?? const <String>[],
2453 defaultValue: contentInsertionConfiguration == null
2454 ? const <String>[]
2455 : kDefaultContentInsertionMimeTypes,
2456 ),
2457 );
2458 properties.add(
2459 DiagnosticsProperty<List<Locale>?>('hintLocales', hintLocales, defaultValue: null),
2460 );
2461 }
2462 }
2463
2464 /// State for an [EditableText].
2465 class EditableTextState extends State<EditableText>
2466 with
2467 AutomaticKeepAliveClientMixin<EditableText>,
2468 WidgetsBindingObserver,
2469 TickerProviderStateMixin<EditableText>,
2470 TextSelectionDelegate,
2471 TextInputClient
2472 implements AutofillClient {
2473 Timer? _cursorTimer;
2474 AnimationController get _cursorBlinkOpacityController {
2475 return _backingCursorBlinkOpacityController ??= AnimationController(vsync: this)
2476 ..addListener(_onCursorColorTick);
2477 }
2478
2479 AnimationController? _backingCursorBlinkOpacityController;
2480 late final Simulation _iosBlinkCursorSimulation = _DiscreteKeyFrameSimulation.iOSBlinkingCaret();
2481
2482 final ValueNotifier<bool> _cursorVisibilityNotifier = ValueNotifier<bool>(true);
2483 final GlobalKey _editableKey = GlobalKey();
2484
2485 /// Detects whether the clipboard can paste.
2486 final ClipboardStatusNotifier clipboardStatus = kIsWeb
2487 // Web browsers will show a permission dialog when Clipboard.hasStrings is
2488 // called. In an EditableText, this will happen before the paste button is
2489 // clicked, often before the context menu is even shown. To avoid this
2490 // poor user experience, always show the paste button on web.
2491 ? _WebClipboardStatusNotifier()
2492 : ClipboardStatusNotifier();
2493
2494 /// Detects whether the Live Text input is enabled.
2495 ///
2496 /// See also:
2497 /// * [LiveText], where the availability of Live Text input can be obtained.
2498 final LiveTextInputStatusNotifier? _liveTextInputStatus = kIsWeb
2499 ? null
2500 : LiveTextInputStatusNotifier();
2501
2502 TextInputConnection? _textInputConnection;
2503 bool get _hasInputConnection => _textInputConnection?.attached ?? false;
2504
2505 TextSelectionOverlay? _selectionOverlay;
2506 ScrollNotificationObserverState? _scrollNotificationObserver;
2507 ({TextEditingValue value, Rect selectionBounds})? _dataWhenToolbarShowScheduled;
2508 bool _listeningToScrollNotificationObserver = false;
2509
2510 bool get _webContextMenuEnabled => kIsWeb && BrowserContextMenu.enabled;
2511
2512 final GlobalKey _scrollableKey = GlobalKey();
2513 ScrollController? _internalScrollController;
2514 ScrollController get _scrollController =>
2515 widget.scrollController ?? (_internalScrollController ??= ScrollController());
2516
2517 final LayerLink _toolbarLayerLink = LayerLink();
2518 final LayerLink _startHandleLayerLink = LayerLink();
2519 final LayerLink _endHandleLayerLink = LayerLink();
2520
2521 bool _didAutoFocus = false;
2522
2523 AutofillGroupState? _currentAutofillScope;
2524 @override
2525 AutofillScope? get currentAutofillScope => _currentAutofillScope;
2526
2527 AutofillClient get _effectiveAutofillClient => widget.autofillClient ?? this;
2528
2529 late SpellCheckConfiguration _spellCheckConfiguration;
2530 late TextStyle _style;
2531
2532 /// Configuration that determines how spell check will be performed.
2533 ///
2534 /// If possible, this configuration will contain a default for the
2535 /// [SpellCheckService] if it is not otherwise specified.
2536 ///
2537 /// See also:
2538 /// * [DefaultSpellCheckService], the spell check service used by default.
2539 @visibleForTesting
2540 SpellCheckConfiguration get spellCheckConfiguration => _spellCheckConfiguration;
2541
2542 /// Whether or not spell check is enabled.
2543 ///
2544 /// Spell check is enabled when a [SpellCheckConfiguration] has been specified
2545 /// for the widget.
2546 bool get spellCheckEnabled => _spellCheckConfiguration.spellCheckEnabled;
2547
2548 /// The most up-to-date spell check results for text input.
2549 ///
2550 /// These results will be updated via calls to spell check through a
2551 /// [SpellCheckService] and used by this widget to build the [TextSpan] tree
2552 /// for text input and menus for replacement suggestions of misspelled words.
2553 SpellCheckResults? spellCheckResults;
2554
2555 bool get _spellCheckResultsReceived =>
2556 spellCheckEnabled &&
2557 spellCheckResults != null &&
2558 spellCheckResults!.suggestionSpans.isNotEmpty;
2559
2560 /// The text processing service used to retrieve the native text processing actions.
2561 final ProcessTextService _processTextService = DefaultProcessTextService();
2562
2563 /// The list of native text processing actions provided by the engine.
2564 final List<ProcessTextAction> _processTextActions = <ProcessTextAction>[];
2565
2566 /// Whether to create an input connection with the platform for text editing
2567 /// or not.
2568 ///
2569 /// Read-only input fields do not need a connection with the platform since
2570 /// there's no need for text editing capabilities (e.g. virtual keyboard).
2571 ///
2572 /// On macOS, most of the selection and focus related shortcuts require a
2573 /// connection with the platform because appropriate platform selectors are
2574 /// sent from the engine and translated into intents. For read-only fields
2575 /// those shortcuts should be available (for instance to allow tab traversal).
2576 ///
2577 /// On the web, we always need a connection because we want some browser
2578 /// functionalities to continue to work on read-only input fields like:
2579 /// - Relevant context menu.
2580 /// - cmd/ctrl+c shortcut to copy.
2581 /// - cmd/ctrl+a to select all.
2582 /// - Changing the selection using a physical keyboard.
2583 bool get _shouldCreateInputConnection =>
2584 kIsWeb || defaultTargetPlatform == TargetPlatform.macOS || !widget.readOnly;
2585
2586 // The time it takes for the floating cursor to snap to the text aligned
2587 // cursor position after the user has finished placing it.
2588 static const Duration _floatingCursorResetTime = Duration(milliseconds: 125);
2589
2590 AnimationController? _floatingCursorResetController;
2591
2592 Orientation? _lastOrientation;
2593
2594 bool get _stylusHandwritingEnabled {
2595 // During the deprecation period, respect scribbleEnabled being explicitly
2596 // set.
2597 if (!widget.scribbleEnabled) {
2598 return widget.scribbleEnabled;
2599 }
2600 return widget.stylusHandwritingEnabled;
2601 }
2602
2603 late final AppLifecycleListener _appLifecycleListener;
2604 bool _justResumed = false;
2605
2606 @override
2607 bool get wantKeepAlive => widget.focusNode.hasFocus;
2608
2609 Color get _cursorColor {
2610 final double effectiveOpacity = math.min(
2611 widget.cursorColor.alpha / 255.0,
2612 _cursorBlinkOpacityController.value,
2613 );
2614 return widget.cursorColor.withOpacity(effectiveOpacity);
2615 }
2616
2617 @override
2618 bool get cutEnabled {
2619 if (widget.selectionControls is! TextSelectionHandleControls) {
2620 return widget.toolbarOptions.cut && !widget.readOnly && !widget.obscureText;
2621 }
2622 return !widget.readOnly && !widget.obscureText && !textEditingValue.selection.isCollapsed;
2623 }
2624
2625 @override
2626 bool get copyEnabled {
2627 if (widget.selectionControls is! TextSelectionHandleControls) {
2628 return widget.toolbarOptions.copy && !widget.obscureText;
2629 }
2630 return !widget.obscureText && !textEditingValue.selection.isCollapsed;
2631 }
2632
2633 @override
2634 bool get pasteEnabled {
2635 if (widget.selectionControls is! TextSelectionHandleControls) {
2636 return widget.toolbarOptions.paste && !widget.readOnly;
2637 }
2638 return !widget.readOnly && (clipboardStatus.value == ClipboardStatus.pasteable);
2639 }
2640
2641 @override
2642 bool get selectAllEnabled {
2643 if (widget.selectionControls is! TextSelectionHandleControls) {
2644 return widget.toolbarOptions.selectAll &&
2645 (!widget.readOnly || !widget.obscureText) &&
2646 widget.enableInteractiveSelection;
2647 }
2648
2649 if (!widget.enableInteractiveSelection || (widget.readOnly && widget.obscureText)) {
2650 return false;
2651 }
2652
2653 switch (defaultTargetPlatform) {
2654 case TargetPlatform.macOS:
2655 return false;
2656 case TargetPlatform.iOS:
2657 return textEditingValue.text.isNotEmpty && textEditingValue.selection.isCollapsed;
2658 case TargetPlatform.android:
2659 case TargetPlatform.fuchsia:
2660 case TargetPlatform.linux:
2661 case TargetPlatform.windows:
2662 return textEditingValue.text.isNotEmpty &&
2663 !(textEditingValue.selection.start == 0 &&
2664 textEditingValue.selection.end == textEditingValue.text.length);
2665 }
2666 }
2667
2668 @override
2669 bool get lookUpEnabled {
2670 if (defaultTargetPlatform != TargetPlatform.iOS) {
2671 return false;
2672 }
2673 return !widget.obscureText &&
2674 !textEditingValue.selection.isCollapsed &&
2675 textEditingValue.selection.textInside(textEditingValue.text).trim() != '';
2676 }
2677
2678 @override
2679 bool get searchWebEnabled {
2680 if (defaultTargetPlatform != TargetPlatform.iOS) {
2681 return false;
2682 }
2683
2684 return !widget.obscureText &&
2685 !textEditingValue.selection.isCollapsed &&
2686 textEditingValue.selection.textInside(textEditingValue.text).trim() != '';
2687 }
2688
2689 @override
2690 bool get shareEnabled {
2691 switch (defaultTargetPlatform) {
2692 case TargetPlatform.android:
2693 case TargetPlatform.iOS:
2694 return !widget.obscureText &&
2695 !textEditingValue.selection.isCollapsed &&
2696 textEditingValue.selection.textInside(textEditingValue.text).trim() != '';
2697 case TargetPlatform.macOS:
2698 case TargetPlatform.fuchsia:
2699 case TargetPlatform.linux:
2700 case TargetPlatform.windows:
2701 return false;
2702 }
2703 }
2704
2705 @override
2706 bool get liveTextInputEnabled {
2707 return _liveTextInputStatus?.value == LiveTextInputStatus.enabled &&
2708 !widget.obscureText &&
2709 !widget.readOnly &&
2710 textEditingValue.selection.isCollapsed;
2711 }
2712
2713 void _onChangedClipboardStatus() {
2714 setState(() {
2715 // Inform the widget that the value of clipboardStatus has changed.
2716 });
2717 }
2718
2719 void _onChangedLiveTextInputStatus() {
2720 setState(() {
2721 // Inform the widget that the value of liveTextInputStatus has changed.
2722 });
2723 }
2724
2725 TextEditingValue get _textEditingValueforTextLayoutMetrics {
2726 final Widget? editableWidget = _editableKey.currentContext?.widget;
2727 if (editableWidget is! _Editable) {
2728 throw StateError('_Editable must be mounted.');
2729 }
2730 return editableWidget.value;
2731 }
2732
2733 /// Copy current selection to [Clipboard].
2734 @override
2735 void copySelection(SelectionChangedCause cause) {
2736 final TextSelection selection = textEditingValue.selection;
2737 if (selection.isCollapsed || widget.obscureText) {
2738 return;
2739 }
2740 final String text = textEditingValue.text;
2741 Clipboard.setData(ClipboardData(text: selection.textInside(text)));
2742 if (cause == SelectionChangedCause.toolbar) {
2743 bringIntoView(textEditingValue.selection.extent);
2744 hideToolbar(false);
2745
2746 switch (defaultTargetPlatform) {
2747 case TargetPlatform.iOS:
2748 case TargetPlatform.macOS:
2749 case TargetPlatform.linux:
2750 case TargetPlatform.windows:
2751 break;
2752 case TargetPlatform.android:
2753 case TargetPlatform.fuchsia:
2754 // Collapse the selection and hide the toolbar and handles.
2755 userUpdateTextEditingValue(
2756 TextEditingValue(
2757 text: textEditingValue.text,
2758 selection: TextSelection.collapsed(offset: textEditingValue.selection.end),
2759 ),
2760 SelectionChangedCause.toolbar,
2761 );
2762 }
2763 }
2764 clipboardStatus.update();
2765 }
2766
2767 /// Cut current selection to [Clipboard].
2768 @override
2769 void cutSelection(SelectionChangedCause cause) {
2770 if (widget.readOnly || widget.obscureText) {
2771 return;
2772 }
2773 final TextSelection selection = textEditingValue.selection;
2774 final String text = textEditingValue.text;
2775 if (selection.isCollapsed) {
2776 return;
2777 }
2778 Clipboard.setData(ClipboardData(text: selection.textInside(text)));
2779 _replaceText(ReplaceTextIntent(textEditingValue, '', selection, cause));
2780 if (cause == SelectionChangedCause.toolbar) {
2781 // Schedule a call to bringIntoView() after renderEditable updates.
2782 SchedulerBinding.instance.addPostFrameCallback((_) {
2783 if (mounted) {
2784 bringIntoView(textEditingValue.selection.extent);
2785 }
2786 }, debugLabel: 'EditableText.bringSelectionIntoView');
2787 hideToolbar();
2788 }
2789 clipboardStatus.update();
2790 }
2791
2792 bool get _allowPaste {
2793 return !widget.readOnly && textEditingValue.selection.isValid;
2794 }
2795
2796 /// Paste text from [Clipboard].
2797 @override
2798 Future<void> pasteText(SelectionChangedCause cause) async {
2799 if (!_allowPaste) {
2800 return;
2801 }
2802 // Snapshot the input before using `await`.
2803 // See https://github.com/flutter/flutter/issues/11427
2804 final ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
2805 if (data == null) {
2806 return;
2807 }
2808 _pasteText(cause, data.text!);
2809 }
2810
2811 void _pasteText(SelectionChangedCause cause, String text) {
2812 if (!_allowPaste) {
2813 return;
2814 }
2815
2816 // After the paste, the cursor should be collapsed and located after the
2817 // pasted content.
2818 final TextSelection selection = textEditingValue.selection;
2819 final int lastSelectionIndex = math.max(selection.baseOffset, selection.extentOffset);
2820 final TextEditingValue collapsedTextEditingValue = textEditingValue.copyWith(
2821 selection: TextSelection.collapsed(offset: lastSelectionIndex),
2822 );
2823
2824 userUpdateTextEditingValue(collapsedTextEditingValue.replaced(selection, text), cause);
2825 if (cause == SelectionChangedCause.toolbar) {
2826 // Schedule a call to bringIntoView() after renderEditable updates.
2827 SchedulerBinding.instance.addPostFrameCallback((_) {
2828 if (mounted) {
2829 bringIntoView(textEditingValue.selection.extent);
2830 }
2831 }, debugLabel: 'EditableText.bringSelectionIntoView');
2832 hideToolbar();
2833 }
2834 }
2835
2836 /// Select the entire text value.
2837 @override
2838 void selectAll(SelectionChangedCause cause) {
2839 if (widget.readOnly && widget.obscureText) {
2840 // If we can't modify it, and we can't copy it, there's no point in
2841 // selecting it.
2842 return;
2843 }
2844 userUpdateTextEditingValue(
2845 textEditingValue.copyWith(
2846 selection: TextSelection(baseOffset: 0, extentOffset: textEditingValue.text.length),
2847 ),
2848 cause,
2849 );
2850
2851 if (cause == SelectionChangedCause.toolbar) {
2852 switch (defaultTargetPlatform) {
2853 case TargetPlatform.android:
2854 case TargetPlatform.iOS:
2855 case TargetPlatform.fuchsia:
2856 break;
2857 case TargetPlatform.macOS:
2858 case TargetPlatform.linux:
2859 case TargetPlatform.windows:
2860 hideToolbar();
2861 }
2862 switch (defaultTargetPlatform) {
2863 case TargetPlatform.android:
2864 case TargetPlatform.fuchsia:
2865 case TargetPlatform.linux:
2866 case TargetPlatform.windows:
2867 bringIntoView(textEditingValue.selection.extent);
2868 case TargetPlatform.macOS:
2869 case TargetPlatform.iOS:
2870 break;
2871 }
2872 }
2873 }
2874
2875 /// Look up the current selection,
2876 /// as in the "Look Up" edit menu button on iOS.
2877 ///
2878 /// Currently this is only implemented for iOS.
2879 ///
2880 /// Throws an error if the selection is empty or collapsed.
2881 Future<void> lookUpSelection(SelectionChangedCause cause) async {
2882 assert(!widget.obscureText);
2883
2884 final String text = textEditingValue.selection.textInside(textEditingValue.text);
2885 if (widget.obscureText || text.isEmpty) {
2886 return;
2887 }
2888 await SystemChannels.platform.invokeMethod('LookUp.invoke', text);
2889 }
2890
2891 /// Launch a web search on the current selection,
2892 /// as in the "Search Web" edit menu button on iOS.
2893 ///
2894 /// Currently this is only implemented for iOS.
2895 ///
2896 /// When 'obscureText' is true or the selection is empty,
2897 /// this function will not do anything
2898 Future<void> searchWebForSelection(SelectionChangedCause cause) async {
2899 assert(!widget.obscureText);
2900 if (widget.obscureText) {
2901 return;
2902 }
2903
2904 final String text = textEditingValue.selection.textInside(textEditingValue.text);
2905 if (text.isNotEmpty) {
2906 await SystemChannels.platform.invokeMethod('SearchWeb.invoke', text);
2907 }
2908 }
2909
2910 /// Launch the share interface for the current selection,
2911 /// as in the "Share..." edit menu button on iOS.
2912 ///
2913 /// Currently this is only implemented for iOS and Android.
2914 ///
2915 /// When 'obscureText' is true or the selection is empty,
2916 /// this function will not do anything
2917 Future<void> shareSelection(SelectionChangedCause cause) async {
2918 assert(!widget.obscureText);
2919 if (widget.obscureText) {
2920 return;
2921 }
2922
2923 final String text = textEditingValue.selection.textInside(textEditingValue.text);
2924 if (text.isNotEmpty) {
2925 await SystemChannels.platform.invokeMethod('Share.invoke', text);
2926 }
2927 }
2928
2929 void _startLiveTextInput(SelectionChangedCause cause) {
2930 if (!liveTextInputEnabled) {
2931 return;
2932 }
2933 if (_hasInputConnection) {
2934 LiveText.startLiveTextInput();
2935 }
2936 if (cause == SelectionChangedCause.toolbar) {
2937 hideToolbar();
2938 }
2939 }
2940
2941 /// Finds specified [SuggestionSpan] that matches the provided index using
2942 /// binary search.
2943 ///
2944 /// See also:
2945 ///
2946 /// * [SpellCheckSuggestionsToolbar], the Material style spell check
2947 /// suggestions toolbar that uses this method to render the correct
2948 /// suggestions in the toolbar for a misspelled word.
2949 SuggestionSpan? findSuggestionSpanAtCursorIndex(int cursorIndex) {
2950 if (!_spellCheckResultsReceived ||
2951 spellCheckResults!.suggestionSpans.last.range.end < cursorIndex) {
2952 // No spell check results have been received or the cursor index is out
2953 // of range that suggestionSpans covers.
2954 return null;
2955 }
2956
2957 final List<SuggestionSpan> suggestionSpans = spellCheckResults!.suggestionSpans;
2958 int leftIndex = 0;
2959 int rightIndex = suggestionSpans.length - 1;
2960 int midIndex = 0;
2961
2962 while (leftIndex <= rightIndex) {
2963 midIndex = ((leftIndex + rightIndex) / 2).floor();
2964 final int currentSpanStart = suggestionSpans[midIndex].range.start;
2965 final int currentSpanEnd = suggestionSpans[midIndex].range.end;
2966
2967 if (cursorIndex <= currentSpanEnd && cursorIndex >= currentSpanStart) {
2968 return suggestionSpans[midIndex];
2969 } else if (cursorIndex <= currentSpanStart) {
2970 rightIndex = midIndex - 1;
2971 } else {
2972 leftIndex = midIndex + 1;
2973 }
2974 }
2975 return null;
2976 }
2977
2978 /// Infers the [SpellCheckConfiguration] used to perform spell check.
2979 ///
2980 /// If spell check is enabled, this will try to infer a value for
2981 /// the [SpellCheckService] if left unspecified.
2982 static SpellCheckConfiguration _inferSpellCheckConfiguration(
2983 SpellCheckConfiguration? configuration,
2984 ) {
2985 final SpellCheckService? spellCheckService = configuration?.spellCheckService;
2986 final bool spellCheckAutomaticallyDisabled =
2987 configuration == null || configuration == const SpellCheckConfiguration.disabled();
2988 final bool spellCheckServiceIsConfigured =
2989 spellCheckService != null ||
2990 WidgetsBinding.instance.platformDispatcher.nativeSpellCheckServiceDefined;
2991 if (spellCheckAutomaticallyDisabled || !spellCheckServiceIsConfigured) {
2992 // Only enable spell check if a non-disabled configuration is provided
2993 // and if that configuration does not specify a spell check service,
2994 // a native spell checker must be supported.
2995 assert(() {
2996 if (!spellCheckAutomaticallyDisabled && !spellCheckServiceIsConfigured) {
2997 FlutterError.reportError(
2998 FlutterErrorDetails(
2999 exception: FlutterError(
3000 'Spell check was enabled with spellCheckConfiguration, but the '
3001 'current platform does not have a supported spell check '
3002 'service, and none was provided. Consider disabling spell '
3003 'check for this platform or passing a SpellCheckConfiguration '
3004 'with a specified spell check service.',
3005 ),
3006 library: 'widget library',
3007 stack: StackTrace.current,
3008 ),
3009 );
3010 }
3011 return true;
3012 }());
3013 return const SpellCheckConfiguration.disabled();
3014 }
3015
3016 return configuration.copyWith(
3017 spellCheckService: spellCheckService ?? DefaultSpellCheckService(),
3018 );
3019 }
3020
3021 /// Returns the [ContextMenuButtonItem]s for the given [ToolbarOptions].
3022 @Deprecated(
3023 'Use `contextMenuBuilder` instead of `toolbarOptions`. '
3024 'This feature was deprecated after v3.3.0-0.5.pre.',
3025 )
3026 List<ContextMenuButtonItem>? buttonItemsForToolbarOptions([TargetPlatform? targetPlatform]) {
3027 final ToolbarOptions toolbarOptions = widget.toolbarOptions;
3028 if (toolbarOptions == ToolbarOptions.empty) {
3029 return null;
3030 }
3031 return <ContextMenuButtonItem>[
3032 if (toolbarOptions.cut && cutEnabled)
3033 ContextMenuButtonItem(
3034 onPressed: () {
3035 cutSelection(SelectionChangedCause.toolbar);
3036 },
3037 type: ContextMenuButtonType.cut,
3038 ),
3039 if (toolbarOptions.copy && copyEnabled)
3040 ContextMenuButtonItem(
3041 onPressed: () {
3042 copySelection(SelectionChangedCause.toolbar);
3043 },
3044 type: ContextMenuButtonType.copy,
3045 ),
3046 if (toolbarOptions.paste && pasteEnabled)
3047 ContextMenuButtonItem(
3048 onPressed: () {
3049 pasteText(SelectionChangedCause.toolbar);
3050 },
3051 type: ContextMenuButtonType.paste,
3052 ),
3053 if (toolbarOptions.selectAll && selectAllEnabled)
3054 ContextMenuButtonItem(
3055 onPressed: () {
3056 selectAll(SelectionChangedCause.toolbar);
3057 },
3058 type: ContextMenuButtonType.selectAll,
3059 ),
3060 ];
3061 }
3062
3063 /// Gets the line heights at the start and end of the selection for the given
3064 /// [EditableTextState].
3065 ///
3066 /// See also:
3067 ///
3068 /// * [TextSelectionToolbarAnchors.getSelectionRect], which depends on this
3069 /// information.
3070 ({double startGlyphHeight, double endGlyphHeight}) getGlyphHeights() {
3071 final TextSelection selection = textEditingValue.selection;
3072
3073 // Only calculate handle rects if the text in the previous frame
3074 // is the same as the text in the current frame. This is done because
3075 // widget.renderObject contains the renderEditable from the previous frame.
3076 // If the text changed between the current and previous frames then
3077 // widget.renderObject.getRectForComposingRange might fail. In cases where
3078 // the current frame is different from the previous we fall back to
3079 // renderObject.preferredLineHeight.
3080 final InlineSpan span = renderEditable.text!;
3081 final String prevText = span.toPlainText();
3082 final String currText = textEditingValue.text;
3083 if (prevText != currText || !selection.isValid || selection.isCollapsed) {
3084 return (
3085 startGlyphHeight: renderEditable.preferredLineHeight,
3086 endGlyphHeight: renderEditable.preferredLineHeight,
3087 );
3088 }
3089
3090 final String selectedGraphemes = selection.textInside(currText);
3091 final int firstSelectedGraphemeExtent = selectedGraphemes.characters.first.length;
3092 final Rect? startCharacterRect = renderEditable.getRectForComposingRange(
3093 TextRange(start: selection.start, end: selection.start + firstSelectedGraphemeExtent),
3094 );
3095 final int lastSelectedGraphemeExtent = selectedGraphemes.characters.last.length;
3096 final Rect? endCharacterRect = renderEditable.getRectForComposingRange(
3097 TextRange(start: selection.end - lastSelectedGraphemeExtent, end: selection.end),
3098 );
3099 return (
3100 startGlyphHeight: startCharacterRect?.height ?? renderEditable.preferredLineHeight,
3101 endGlyphHeight: endCharacterRect?.height ?? renderEditable.preferredLineHeight,
3102 );
3103 }
3104
3105 /// {@template flutter.widgets.EditableText.getAnchors}
3106 /// Returns the anchor points for the default context menu.
3107 /// {@endtemplate}
3108 ///
3109 /// See also:
3110 ///
3111 /// * [contextMenuButtonItems], which provides the [ContextMenuButtonItem]s
3112 /// for the default context menu buttons.
3113 TextSelectionToolbarAnchors get contextMenuAnchors {
3114 if (renderEditable.lastSecondaryTapDownPosition != null) {
3115 return TextSelectionToolbarAnchors(
3116 primaryAnchor: renderEditable.lastSecondaryTapDownPosition!,
3117 );
3118 }
3119
3120 final (startGlyphHeight: double startGlyphHeight, endGlyphHeight: double endGlyphHeight) =
3121 getGlyphHeights();
3122 final TextSelection selection = textEditingValue.selection;
3123 final List<TextSelectionPoint> points = renderEditable.getEndpointsForSelection(selection);
3124 return TextSelectionToolbarAnchors.fromSelection(
3125 renderBox: renderEditable,
3126 startGlyphHeight: startGlyphHeight,
3127 endGlyphHeight: endGlyphHeight,
3128 selectionEndpoints: points,
3129 );
3130 }
3131
3132 /// Returns the [ContextMenuButtonItem]s representing the buttons in this
3133 /// platform's default selection menu for [EditableText].
3134 ///
3135 /// See also:
3136 ///
3137 /// * [EditableText.getEditableButtonItems], which performs a similar role,
3138 /// but for any editable field, not just specifically EditableText.
3139 /// * [SystemContextMenu.getDefaultItems], which performs a similar role, but
3140 /// for the system-rendered context menu.
3141 /// * [SelectableRegionState.contextMenuButtonItems], which performs a similar
3142 /// role but for content that is selectable but not editable.
3143 /// * [contextMenuAnchors], which provides the anchor points for the default
3144 /// context menu.
3145 /// * [AdaptiveTextSelectionToolbar], which builds the toolbar itself, and can
3146 /// take a list of [ContextMenuButtonItem]s with
3147 /// [AdaptiveTextSelectionToolbar.buttonItems].
3148 /// * [AdaptiveTextSelectionToolbar.getAdaptiveButtons], which builds the
3149 /// button Widgets for the current platform given [ContextMenuButtonItem]s.
3150 List<ContextMenuButtonItem> get contextMenuButtonItems {
3151 return buttonItemsForToolbarOptions() ??
3152 EditableText.getEditableButtonItems(
3153 clipboardStatus: clipboardStatus.value,
3154 onCopy: copyEnabled ? () => copySelection(SelectionChangedCause.toolbar) : null,
3155 onCut: cutEnabled ? () => cutSelection(SelectionChangedCause.toolbar) : null,
3156 onPaste: pasteEnabled ? () => pasteText(SelectionChangedCause.toolbar) : null,
3157 onSelectAll: selectAllEnabled ? () => selectAll(SelectionChangedCause.toolbar) : null,
3158 onLookUp: lookUpEnabled ? () => lookUpSelection(SelectionChangedCause.toolbar) : null,
3159 onSearchWeb: searchWebEnabled
3160 ? () => searchWebForSelection(SelectionChangedCause.toolbar)
3161 : null,
3162 onShare: shareEnabled ? () => shareSelection(SelectionChangedCause.toolbar) : null,
3163 onLiveTextInput: liveTextInputEnabled
3164 ? () => _startLiveTextInput(SelectionChangedCause.toolbar)
3165 : null,
3166 )
3167 ..addAll(_textProcessingActionButtonItems);
3168 }
3169
3170 List<ContextMenuButtonItem> get _textProcessingActionButtonItems {
3171 final List<ContextMenuButtonItem> buttonItems = <ContextMenuButtonItem>[];
3172 final TextSelection selection = textEditingValue.selection;
3173 if (widget.obscureText || !selection.isValid || selection.isCollapsed) {
3174 return buttonItems;
3175 }
3176
3177 for (final ProcessTextAction action in _processTextActions) {
3178 buttonItems.add(
3179 ContextMenuButtonItem(
3180 label: action.label,
3181 onPressed: () async {
3182 final String selectedText = selection.textInside(textEditingValue.text);
3183 if (selectedText.isNotEmpty) {
3184 final String? processedText = await _processTextService.processTextAction(
3185 action.id,
3186 selectedText,
3187 widget.readOnly,
3188 );
3189 // If an activity does not return a modified version, just hide the toolbar.
3190 // Otherwise use the result to replace the selected text.
3191 if (processedText != null && _allowPaste) {
3192 _pasteText(SelectionChangedCause.toolbar, processedText);
3193 } else {
3194 hideToolbar();
3195 }
3196 }
3197 },
3198 ),
3199 );
3200 }
3201 return buttonItems;
3202 }
3203
3204 // State lifecycle:
3205
3206 @protected
3207 @override
3208 void initState() {
3209 super.initState();
3210 _liveTextInputStatus?.addListener(_onChangedLiveTextInputStatus);
3211 clipboardStatus.addListener(_onChangedClipboardStatus);
3212 widget.controller.addListener(_didChangeTextEditingValue);
3213 widget.focusNode.addListener(_handleFocusChanged);
3214 _cursorVisibilityNotifier.value = widget.showCursor;
3215 _spellCheckConfiguration = _inferSpellCheckConfiguration(widget.spellCheckConfiguration);
3216 _appLifecycleListener = AppLifecycleListener(onResume: () => _justResumed = true);
3217 _initProcessTextActions();
3218 }
3219
3220 /// Query the engine to initialize the list of text processing actions to show
3221 /// in the text selection toolbar.
3222 Future<void> _initProcessTextActions() async {
3223 _processTextActions.clear();
3224 _processTextActions.addAll(await _processTextService.queryTextActions());
3225 }
3226
3227 // Whether `TickerMode.of(context)` is true and animations (like blinking the
3228 // cursor) are supposed to run.
3229 bool _tickersEnabled = true;
3230
3231 @protected
3232 @override
3233 void didChangeDependencies() {
3234 super.didChangeDependencies();
3235
3236 _style = MediaQuery.boldTextOf(context)
3237 ? widget.style.merge(const TextStyle(fontWeight: FontWeight.bold))
3238 : widget.style;
3239
3240 final AutofillGroupState? newAutofillGroup = AutofillGroup.maybeOf(context);
3241 if (currentAutofillScope != newAutofillGroup) {
3242 _currentAutofillScope?.unregister(autofillId);
3243 _currentAutofillScope = newAutofillGroup;
3244 _currentAutofillScope?.register(_effectiveAutofillClient);
3245 }
3246
3247 if (!_didAutoFocus && widget.autofocus) {
3248 _didAutoFocus = true;
3249 SchedulerBinding.instance.addPostFrameCallback((_) {
3250 if (mounted && renderEditable.hasSize) {
3251 _flagInternalFocus();
3252 FocusScope.of(context).autofocus(widget.focusNode);
3253 }
3254 }, debugLabel: 'EditableText.autofocus');
3255 }
3256
3257 // Restart or stop the blinking cursor when TickerMode changes.
3258 final bool newTickerEnabled = TickerMode.of(context);
3259 if (_tickersEnabled != newTickerEnabled) {
3260 _tickersEnabled = newTickerEnabled;
3261 if (_showBlinkingCursor) {
3262 _startCursorBlink();
3263 } else if (!_tickersEnabled && _cursorTimer != null) {
3264 _stopCursorBlink();
3265 }
3266 }
3267
3268 // Check for changes in viewId.
3269 if (_hasInputConnection) {
3270 final int newViewId = View.of(context).viewId;
3271 if (newViewId != _viewId) {
3272 _textInputConnection!.updateConfig(_effectiveAutofillClient.textInputConfiguration);
3273 }
3274 }
3275
3276 if (defaultTargetPlatform != TargetPlatform.iOS &&
3277 defaultTargetPlatform != TargetPlatform.android) {
3278 return;
3279 }
3280
3281 // Hide the text selection toolbar on mobile when orientation changes.
3282 final Orientation orientation = MediaQuery.orientationOf(context);
3283 if (_lastOrientation == null) {
3284 _lastOrientation = orientation;
3285 return;
3286 }
3287 if (orientation != _lastOrientation) {
3288 _lastOrientation = orientation;
3289 if (defaultTargetPlatform == TargetPlatform.iOS) {
3290 hideToolbar(false);
3291 }
3292 if (defaultTargetPlatform == TargetPlatform.android) {
3293 hideToolbar();
3294 }
3295 }
3296
3297 if (_listeningToScrollNotificationObserver) {
3298 // Only update subscription when we have previously subscribed to the
3299 // scroll notification observer. We only subscribe to the scroll
3300 // notification observer when the context menu is shown on platforms that
3301 // support _platformSupportsFadeOnScroll.
3302 _scrollNotificationObserver?.removeListener(_handleContextMenuOnParentScroll);
3303 _scrollNotificationObserver = ScrollNotificationObserver.maybeOf(context);
3304 _scrollNotificationObserver?.addListener(_handleContextMenuOnParentScroll);
3305 }
3306 }
3307
3308 @protected
3309 @override
3310 void didUpdateWidget(EditableText oldWidget) {
3311 super.didUpdateWidget(oldWidget);
3312 if (widget.controller != oldWidget.controller) {
3313 oldWidget.controller.removeListener(_didChangeTextEditingValue);
3314 widget.controller.addListener(_didChangeTextEditingValue);
3315 _updateRemoteEditingValueIfNeeded();
3316 }
3317
3318 if (_selectionOverlay != null &&
3319 (widget.contextMenuBuilder != oldWidget.contextMenuBuilder ||
3320 widget.selectionControls != oldWidget.selectionControls ||
3321 widget.onSelectionHandleTapped != oldWidget.onSelectionHandleTapped ||
3322 widget.dragStartBehavior != oldWidget.dragStartBehavior ||
3323 widget.magnifierConfiguration != oldWidget.magnifierConfiguration)) {
3324 final bool shouldShowToolbar = _selectionOverlay!.toolbarIsVisible;
3325 final bool shouldShowHandles = _selectionOverlay!.handlesVisible;
3326 _selectionOverlay!.dispose();
3327 _selectionOverlay = _createSelectionOverlay();
3328 if (shouldShowToolbar || shouldShowHandles) {
3329 SchedulerBinding.instance.addPostFrameCallback((Duration _) {
3330 if (shouldShowToolbar) {
3331 _selectionOverlay!.showToolbar();
3332 }
3333 if (shouldShowHandles) {
3334 _selectionOverlay!.showHandles();
3335 }
3336 });
3337 }
3338 } else if (widget.controller.selection != oldWidget.controller.selection) {
3339 _selectionOverlay?.update(_value);
3340 }
3341 _selectionOverlay?.handlesVisible = widget.showSelectionHandles;
3342
3343 if (widget.autofillClient != oldWidget.autofillClient) {
3344 _currentAutofillScope?.unregister(oldWidget.autofillClient?.autofillId ?? autofillId);
3345 _currentAutofillScope?.register(_effectiveAutofillClient);
3346 }
3347
3348 if (widget.focusNode != oldWidget.focusNode) {
3349 oldWidget.focusNode.removeListener(_handleFocusChanged);
3350 widget.focusNode.addListener(_handleFocusChanged);
3351 updateKeepAlive();
3352 }
3353
3354 if (!_shouldCreateInputConnection) {
3355 _closeInputConnectionIfNeeded();
3356 } else if (oldWidget.readOnly && _hasFocus) {
3357 // _openInputConnection must be called after layout information is available.
3358 // See https://github.com/flutter/flutter/issues/126312
3359 SchedulerBinding.instance.addPostFrameCallback((Duration _) {
3360 _openInputConnection();
3361 }, debugLabel: 'EditableText.openInputConnection');
3362 }
3363
3364 if (kIsWeb && _hasInputConnection) {
3365 if (oldWidget.readOnly != widget.readOnly) {
3366 _textInputConnection!.updateConfig(_effectiveAutofillClient.textInputConfiguration);
3367 }
3368 }
3369
3370 if (_hasInputConnection) {
3371 if (oldWidget.obscureText != widget.obscureText ||
3372 oldWidget.keyboardType != widget.keyboardType) {
3373 _textInputConnection!.updateConfig(_effectiveAutofillClient.textInputConfiguration);
3374 }
3375 }
3376
3377 if (widget.style != oldWidget.style) {
3378 // The _textInputConnection will pick up the new style when it attaches in
3379 // _openInputConnection.
3380 _style = MediaQuery.boldTextOf(context)
3381 ? widget.style.merge(const TextStyle(fontWeight: FontWeight.bold))
3382 : widget.style;
3383 if (_hasInputConnection) {
3384 _textInputConnection!.setStyle(
3385 fontFamily: _style.fontFamily,
3386 fontSize: _style.fontSize,
3387 fontWeight: _style.fontWeight,
3388 textDirection: _textDirection,
3389 textAlign: widget.textAlign,
3390 );
3391 }
3392 }
3393
3394 if (widget.showCursor != oldWidget.showCursor) {
3395 _startOrStopCursorTimerIfNeeded();
3396 }
3397 final bool canPaste = widget.selectionControls is TextSelectionHandleControls
3398 ? pasteEnabled
3399 : widget.selectionControls?.canPaste(this) ?? false;
3400 if (widget.selectionEnabled && pasteEnabled && canPaste) {
3401 clipboardStatus.update();
3402 }
3403 }
3404
3405 void _disposeScrollNotificationObserver() {
3406 _listeningToScrollNotificationObserver = false;
3407 if (_scrollNotificationObserver != null) {
3408 _scrollNotificationObserver!.removeListener(_handleContextMenuOnParentScroll);
3409 _scrollNotificationObserver = null;
3410 }
3411 }
3412
3413 @protected
3414 @override
3415 void dispose() {
3416 _internalScrollController?.dispose();
3417 _currentAutofillScope?.unregister(autofillId);
3418 widget.controller.removeListener(_didChangeTextEditingValue);
3419 _floatingCursorResetController?.dispose();
3420 _floatingCursorResetController = null;
3421 _closeInputConnectionIfNeeded();
3422 assert(!_hasInputConnection);
3423 _cursorTimer?.cancel();
3424 _cursorTimer = null;
3425 _backingCursorBlinkOpacityController?.dispose();
3426 _backingCursorBlinkOpacityController = null;
3427 _selectionOverlay?.dispose();
3428 _selectionOverlay = null;
3429 widget.focusNode.removeListener(_handleFocusChanged);
3430 WidgetsBinding.instance.removeObserver(this);
3431 _liveTextInputStatus?.removeListener(_onChangedLiveTextInputStatus);
3432 _liveTextInputStatus?.dispose();
3433 clipboardStatus.removeListener(_onChangedClipboardStatus);
3434 clipboardStatus.dispose();
3435 _cursorVisibilityNotifier.dispose();
3436 _appLifecycleListener.dispose();
3437 FocusManager.instance.removeListener(_unflagInternalFocus);
3438 _disposeScrollNotificationObserver();
3439 super.dispose();
3440 assert(_batchEditDepth <= 0, 'unfinished batch edits: $_batchEditDepth');
3441 }
3442
3443 // TextInputClient implementation:
3444
3445 /// The last known [TextEditingValue] of the platform text input plugin.
3446 ///
3447 /// This value is updated when the platform text input plugin sends a new
3448 /// update via [updateEditingValue], or when [EditableText] calls
3449 /// [TextInputConnection.setEditingState] to overwrite the platform text input
3450 /// plugin's [TextEditingValue].
3451 ///
3452 /// Used in [_updateRemoteEditingValueIfNeeded] to determine whether the
3453 /// remote value is outdated and needs updating.
3454 TextEditingValue? _lastKnownRemoteTextEditingValue;
3455
3456 @override
3457 TextEditingValue get currentTextEditingValue => _value;
3458
3459 @override
3460 void updateEditingValue(TextEditingValue value) {
3461 // This method handles text editing state updates from the platform text
3462 // input plugin. The [EditableText] may not have the focus or an open input
3463 // connection, as autofill can update a disconnected [EditableText].
3464
3465 // Since we still have to support keyboard select, this is the best place
3466 // to disable text updating.
3467 if (!_shouldCreateInputConnection) {
3468 return;
3469 }
3470
3471 if (_checkNeedsAdjustAffinity(value)) {
3472 value = value.copyWith(
3473 selection: value.selection.copyWith(affinity: _value.selection.affinity),
3474 );
3475 }
3476
3477 if (widget.readOnly) {
3478 // In the read-only case, we only care about selection changes, and reject
3479 // everything else.
3480 value = _value.copyWith(selection: value.selection);
3481 }
3482 _lastKnownRemoteTextEditingValue = value;
3483
3484 if (value == _value) {
3485 // This is possible, for example, when the numeric keyboard is input,
3486 // the engine will notify twice for the same value.
3487 // Track at https://github.com/flutter/flutter/issues/65811
3488 return;
3489 }
3490
3491 if (value.text == _value.text && value.composing == _value.composing) {
3492 // `selection` is the only change.
3493 SelectionChangedCause cause;
3494 if (_textInputConnection?.scribbleInProgress ?? false) {
3495 cause = SelectionChangedCause.stylusHandwriting;
3496 } else if (_pointOffsetOrigin != null) {
3497 // For floating cursor selection when force pressing the space bar.
3498 cause = SelectionChangedCause.forcePress;
3499 } else {
3500 cause = SelectionChangedCause.keyboard;
3501 }
3502 _handleSelectionChanged(value.selection, cause);
3503 } else {
3504 if (value.text != _value.text) {
3505 // Hide the toolbar if the text was changed, but only hide the toolbar
3506 // overlay; the selection handle's visibility will be handled
3507 // by `_handleSelectionChanged`. https://github.com/flutter/flutter/issues/108673
3508 hideToolbar(false);
3509 }
3510 _currentPromptRectRange = null;
3511
3512 final bool revealObscuredInput =
3513 _hasInputConnection &&
3514 widget.obscureText &&
3515 WidgetsBinding.instance.platformDispatcher.brieflyShowPassword &&
3516 value.text.length == _value.text.length + 1;
3517
3518 _obscureShowCharTicksPending = revealObscuredInput ? _kObscureShowLatestCharCursorTicks : 0;
3519 _obscureLatestCharIndex = revealObscuredInput ? _value.selection.baseOffset : null;
3520 _formatAndSetValue(value, SelectionChangedCause.keyboard);
3521 }
3522
3523 if (_showBlinkingCursor && _cursorTimer != null) {
3524 // To keep the cursor from blinking while typing, restart the timer here.
3525 _stopCursorBlink(resetCharTicks: false);
3526 _startCursorBlink();
3527 }
3528
3529 // Wherever the value is changed by the user, schedule a showCaretOnScreen
3530 // to make sure the user can see the changes they just made. Programmatic
3531 // changes to `textEditingValue` do not trigger the behavior even if the
3532 // text field is focused.
3533 _scheduleShowCaretOnScreen(withAnimation: true);
3534 }
3535
3536 bool _checkNeedsAdjustAffinity(TextEditingValue value) {
3537 // Trust the engine affinity if the text changes or selection changes.
3538 return value.text == _value.text &&
3539 value.selection.isCollapsed == _value.selection.isCollapsed &&
3540 value.selection.start == _value.selection.start &&
3541 value.selection.affinity != _value.selection.affinity;
3542 }
3543
3544 @override
3545 void performAction(TextInputAction action) {
3546 switch (action) {
3547 case TextInputAction.newline:
3548 // If this is a multiline EditableText, do nothing for a "newline"
3549 // action; The newline is already inserted. Otherwise, finalize
3550 // editing.
3551 if (!_isMultiline) {
3552 _finalizeEditing(action, shouldUnfocus: true);
3553 }
3554 case TextInputAction.done:
3555 case TextInputAction.go:
3556 case TextInputAction.next:
3557 case TextInputAction.previous:
3558 case TextInputAction.search:
3559 case TextInputAction.send:
3560 _finalizeEditing(action, shouldUnfocus: true);
3561 case TextInputAction.continueAction:
3562 case TextInputAction.emergencyCall:
3563 case TextInputAction.join:
3564 case TextInputAction.none:
3565 case TextInputAction.route:
3566 case TextInputAction.unspecified:
3567 // Finalize editing, but don't give up focus because this keyboard
3568 // action does not imply the user is done inputting information.
3569 _finalizeEditing(action, shouldUnfocus: false);
3570 }
3571 }
3572
3573 @override
3574 void performPrivateCommand(String action, Map<String, dynamic> data) {
3575 widget.onAppPrivateCommand?.call(action, data);
3576 }
3577
3578 @override
3579 void insertContent(KeyboardInsertedContent content) {
3580 assert(
3581 widget.contentInsertionConfiguration?.allowedMimeTypes.contains(content.mimeType) ?? false,
3582 );
3583 widget.contentInsertionConfiguration?.onContentInserted.call(content);
3584 }
3585
3586 // The original position of the caret on FloatingCursorDragState.start.
3587 Offset? _startCaretCenter;
3588
3589 // The most recent text position as determined by the location of the floating
3590 // cursor.
3591 TextPosition? _lastTextPosition;
3592
3593 // The offset of the floating cursor as determined from the start call.
3594 Offset? _pointOffsetOrigin;
3595
3596 // The most recent position of the floating cursor.
3597 Offset? _lastBoundedOffset;
3598
3599 // Because the center of the cursor is preferredLineHeight / 2 below the touch
3600 // origin, but the touch origin is used to determine which line the cursor is
3601 // on, we need this offset to correctly render and move the cursor.
3602 Offset get _floatingCursorOffset => Offset(0, renderEditable.preferredLineHeight / 2);
3603
3604 @override
3605 void updateFloatingCursor(RawFloatingCursorPoint point) {
3606 _floatingCursorResetController ??= AnimationController(vsync: this)
3607 ..addListener(_onFloatingCursorResetTick);
3608 switch (point.state) {
3609 case FloatingCursorDragState.Start:
3610 if (_floatingCursorResetController!.isAnimating) {
3611 _floatingCursorResetController!.stop();
3612 _onFloatingCursorResetTick();
3613 }
3614 // Stop cursor blinking and making it visible.
3615 _stopCursorBlink(resetCharTicks: false);
3616 _cursorBlinkOpacityController.value = 1.0;
3617 // We want to send in points that are centered around a (0,0) origin, so
3618 // we cache the position.
3619 _pointOffsetOrigin = point.offset;
3620
3621 final Offset startCaretCenter;
3622 final TextPosition currentTextPosition;
3623 final bool shouldResetOrigin;
3624 // Only non-null when starting a floating cursor via long press.
3625 if (point.startLocation != null) {
3626 shouldResetOrigin = false;
3627 (startCaretCenter, currentTextPosition) = point.startLocation!;
3628 } else {
3629 shouldResetOrigin = true;
3630 currentTextPosition = TextPosition(
3631 offset: renderEditable.selection!.baseOffset,
3632 affinity: renderEditable.selection!.affinity,
3633 );
3634 startCaretCenter = renderEditable.getLocalRectForCaret(currentTextPosition).center;
3635 }
3636
3637 _startCaretCenter = startCaretCenter;
3638 _lastBoundedOffset = renderEditable.calculateBoundedFloatingCursorOffset(
3639 _startCaretCenter! - _floatingCursorOffset,
3640 shouldResetOrigin: shouldResetOrigin,
3641 );
3642 _lastTextPosition = currentTextPosition;
3643 renderEditable.setFloatingCursor(point.state, _lastBoundedOffset!, _lastTextPosition!);
3644 case FloatingCursorDragState.Update:
3645 final Offset centeredPoint = point.offset! - _pointOffsetOrigin!;
3646 final Offset rawCursorOffset = _startCaretCenter! + centeredPoint - _floatingCursorOffset;
3647
3648 _lastBoundedOffset = renderEditable.calculateBoundedFloatingCursorOffset(rawCursorOffset);
3649 _lastTextPosition = renderEditable.getPositionForPoint(
3650 renderEditable.localToGlobal(_lastBoundedOffset! + _floatingCursorOffset),
3651 );
3652 renderEditable.setFloatingCursor(point.state, _lastBoundedOffset!, _lastTextPosition!);
3653 case FloatingCursorDragState.End:
3654 // Resume cursor blinking.
3655 _startCursorBlink();
3656 // We skip animation if no update has happened.
3657 if (_lastTextPosition != null && _lastBoundedOffset != null) {
3658 _floatingCursorResetController!.value = 0.0;
3659 _floatingCursorResetController!.animateTo(
3660 1.0,
3661 duration: _floatingCursorResetTime,
3662 curve: Curves.decelerate,
3663 );
3664 }
3665 }
3666 }
3667
3668 void _onFloatingCursorResetTick() {
3669 final Offset finalPosition =
3670 renderEditable.getLocalRectForCaret(_lastTextPosition!).centerLeft - _floatingCursorOffset;
3671 if (_floatingCursorResetController!.isCompleted) {
3672 renderEditable.setFloatingCursor(
3673 FloatingCursorDragState.End,
3674 finalPosition,
3675 _lastTextPosition!,
3676 );
3677 // During a floating cursor's move gesture (1 finger), a cursor is
3678 // animated only visually, without actually updating the selection.
3679 // Only after move gesture is complete, this function will be called
3680 // to actually update the selection to the new cursor location with
3681 // zero selection length.
3682
3683 // However, During a floating cursor's selection gesture (2 fingers), the
3684 // selection is constantly updated by the engine throughout the gesture.
3685 // Thus when the gesture is complete, we should not update the selection
3686 // to the cursor location with zero selection length, because that would
3687 // overwrite the selection made by floating cursor selection.
3688
3689 // Here we use `isCollapsed` to distinguish between floating cursor's
3690 // move gesture (1 finger) vs selection gesture (2 fingers), as
3691 // the engine does not provide information other than notifying a
3692 // new selection during with selection gesture (2 fingers).
3693 if (renderEditable.selection!.isCollapsed) {
3694 // The cause is technically the force cursor, but the cause is listed as tap as the desired functionality is the same.
3695 _handleSelectionChanged(
3696 TextSelection.fromPosition(_lastTextPosition!),
3697 SelectionChangedCause.forcePress,
3698 );
3699 }
3700 _startCaretCenter = null;
3701 _lastTextPosition = null;
3702 _pointOffsetOrigin = null;
3703 _lastBoundedOffset = null;
3704 } else {
3705 final double lerpValue = _floatingCursorResetController!.value;
3706 final double lerpX = ui.lerpDouble(_lastBoundedOffset!.dx, finalPosition.dx, lerpValue)!;
3707 final double lerpY = ui.lerpDouble(_lastBoundedOffset!.dy, finalPosition.dy, lerpValue)!;
3708
3709 renderEditable.setFloatingCursor(
3710 FloatingCursorDragState.Update,
3711 Offset(lerpX, lerpY),
3712 _lastTextPosition!,
3713 resetLerpValue: lerpValue,
3714 );
3715 }
3716 }
3717
3718 @pragma('vm:notify-debugger-on-exception')
3719 void _finalizeEditing(TextInputAction action, {required bool shouldUnfocus}) {
3720 // Take any actions necessary now that the user has completed editing.
3721 if (widget.onEditingComplete != null) {
3722 try {
3723 widget.onEditingComplete!();
3724 } catch (exception, stack) {
3725 FlutterError.reportError(
3726 FlutterErrorDetails(
3727 exception: exception,
3728 stack: stack,
3729 library: 'widgets',
3730 context: ErrorDescription('while calling onEditingComplete for $action'),
3731 ),
3732 );
3733 }
3734 } else {
3735 // Default behavior if the developer did not provide an
3736 // onEditingComplete callback: Finalize editing and remove focus, or move
3737 // it to the next/previous field, depending on the action.
3738 widget.controller.clearComposing();
3739 if (shouldUnfocus) {
3740 switch (action) {
3741 case TextInputAction.none:
3742 case TextInputAction.unspecified:
3743 case TextInputAction.done:
3744 case TextInputAction.go:
3745 case TextInputAction.search:
3746 case TextInputAction.send:
3747 case TextInputAction.continueAction:
3748 case TextInputAction.join:
3749 case TextInputAction.route:
3750 case TextInputAction.emergencyCall:
3751 case TextInputAction.newline:
3752 widget.focusNode.unfocus();
3753 case TextInputAction.next:
3754 widget.focusNode.nextFocus();
3755 case TextInputAction.previous:
3756 widget.focusNode.previousFocus();
3757 }
3758 }
3759 }
3760
3761 final ValueChanged<String>? onSubmitted = widget.onSubmitted;
3762 if (onSubmitted == null) {
3763 return;
3764 }
3765
3766 // Invoke optional callback with the user's submitted content.
3767 try {
3768 onSubmitted(_value.text);
3769 } catch (exception, stack) {
3770 FlutterError.reportError(
3771 FlutterErrorDetails(
3772 exception: exception,
3773 stack: stack,
3774 library: 'widgets',
3775 context: ErrorDescription('while calling onSubmitted for $action'),
3776 ),
3777 );
3778 }
3779
3780 // If `shouldUnfocus` is true, the text field should no longer be focused
3781 // after the microtask queue is drained. But in case the developer cancelled
3782 // the focus change in the `onSubmitted` callback by focusing this input
3783 // field again, reset the soft keyboard.
3784 // See https://github.com/flutter/flutter/issues/84240.
3785 //
3786 // `_restartConnectionIfNeeded` creates a new TextInputConnection to replace
3787 // the current one. This on iOS switches to a new input view and on Android
3788 // restarts the input method, and in both cases the soft keyboard will be
3789 // reset.
3790 if (shouldUnfocus) {
3791 _scheduleRestartConnection();
3792 }
3793 }
3794
3795 int _batchEditDepth = 0;
3796
3797 /// Begins a new batch edit, within which new updates made to the text editing
3798 /// value will not be sent to the platform text input plugin.
3799 ///
3800 /// Batch edits nest. When the outermost batch edit finishes, [endBatchEdit]
3801 /// will attempt to send [currentTextEditingValue] to the text input plugin if
3802 /// it detected a change.
3803 void beginBatchEdit() {
3804 _batchEditDepth += 1;
3805 }
3806
3807 /// Ends the current batch edit started by the last call to [beginBatchEdit],
3808 /// and send [currentTextEditingValue] to the text input plugin if needed.
3809 ///
3810 /// Throws an error in debug mode if this [EditableText] is not in a batch
3811 /// edit.
3812 void endBatchEdit() {
3813 _batchEditDepth -= 1;
3814 assert(
3815 _batchEditDepth >= 0,
3816 'Unbalanced call to endBatchEdit: beginBatchEdit must be called first.',
3817 );
3818 _updateRemoteEditingValueIfNeeded();
3819 }
3820
3821 void _updateRemoteEditingValueIfNeeded() {
3822 if (_batchEditDepth > 0 || !_hasInputConnection) {
3823 return;
3824 }
3825 final TextEditingValue localValue = _value;
3826 if (localValue == _lastKnownRemoteTextEditingValue) {
3827 return;
3828 }
3829 _textInputConnection!.setEditingState(localValue);
3830 _lastKnownRemoteTextEditingValue = localValue;
3831 }
3832
3833 TextEditingValue get _value => widget.controller.value;
3834 set _value(TextEditingValue value) {
3835 widget.controller.value = value;
3836 }
3837
3838 bool get _hasFocus => widget.focusNode.hasFocus;
3839 bool get _isMultiline => widget.maxLines != 1;
3840
3841 /// Flag to track whether this [EditableText] was in focus when [onTapOutside]
3842 /// was called.
3843 ///
3844 /// This is used to determine whether [onTapUpOutside] should be called.
3845 /// The reason [_hasFocus] can't be used directly is because [onTapOutside]
3846 /// might unfocus this [EditableText] and block the [onTapUpOutside] call.
3847 bool _hadFocusOnTapDown = false;
3848
3849 // Finds the closest scroll offset to the current scroll offset that fully
3850 // reveals the given caret rect. If the given rect's main axis extent is too
3851 // large to be fully revealed in `renderEditable`, it will be centered along
3852 // the main axis.
3853 //
3854 // If this is a multiline EditableText (which means the Editable can only
3855 // scroll vertically), the given rect's height will first be extended to match
3856 // `renderEditable.preferredLineHeight`, before the target scroll offset is
3857 // calculated.
3858 RevealedOffset _getOffsetToRevealCaret(Rect rect) {
3859 if (!_scrollController.position.allowImplicitScrolling) {
3860 return RevealedOffset(offset: _scrollController.offset, rect: rect);
3861 }
3862
3863 final Size editableSize = renderEditable.size;
3864 final double additionalOffset;
3865 final Offset unitOffset;
3866
3867 if (!_isMultiline) {
3868 additionalOffset = rect.width >= editableSize.width
3869 // Center `rect` if it's oversized.
3870 ? editableSize.width / 2 - rect.center.dx
3871 // Valid additional offsets range from (rect.right - size.width)
3872 // to (rect.left). Pick the closest one if out of range.
3873 : clampDouble(0.0, rect.right - editableSize.width, rect.left);
3874 unitOffset = const Offset(1, 0);
3875 } else {
3876 // The caret is vertically centered within the line. Expand the caret's
3877 // height so that it spans the line because we're going to ensure that the
3878 // entire expanded caret is scrolled into view.
3879 final Rect expandedRect = Rect.fromCenter(
3880 center: rect.center,
3881 width: rect.width,
3882 height: math.max(rect.height, renderEditable.preferredLineHeight),
3883 );
3884
3885 additionalOffset = expandedRect.height >= editableSize.height
3886 ? editableSize.height / 2 - expandedRect.center.dy
3887 : clampDouble(0.0, expandedRect.bottom - editableSize.height, expandedRect.top);
3888 unitOffset = const Offset(0, 1);
3889 }
3890
3891 // No overscrolling when encountering tall fonts/scripts that extend past
3892 // the ascent.
3893 final double targetOffset = clampDouble(
3894 additionalOffset + _scrollController.offset,
3895 _scrollController.position.minScrollExtent,
3896 _scrollController.position.maxScrollExtent,
3897 );
3898
3899 final double offsetDelta = _scrollController.offset - targetOffset;
3900 return RevealedOffset(rect: rect.shift(unitOffset * offsetDelta), offset: targetOffset);
3901 }
3902
3903 /// Whether to send the autofill information to the autofill service. True by
3904 /// default.
3905 bool get _needsAutofill =>
3906 _effectiveAutofillClient.textInputConfiguration.autofillConfiguration.enabled;
3907
3908 // Must be called after layout.
3909 // See https://github.com/flutter/flutter/issues/126312
3910 void _openInputConnection() {
3911 if (!_shouldCreateInputConnection) {
3912 return;
3913 }
3914 if (!_hasInputConnection) {
3915 final TextEditingValue localValue = _value;
3916
3917 // When _needsAutofill == true && currentAutofillScope == null, autofill
3918 // is allowed but saving the user input from the text field is
3919 // discouraged.
3920 //
3921 // In case the autofillScope changes from a non-null value to null, or
3922 // _needsAutofill changes to false from true, the platform needs to be
3923 // notified to exclude this field from the autofill context. So we need to
3924 // provide the autofillId.
3925 _textInputConnection = _needsAutofill && currentAutofillScope != null
3926 ? currentAutofillScope!.attach(this, _effectiveAutofillClient.textInputConfiguration)
3927 : TextInput.attach(this, _effectiveAutofillClient.textInputConfiguration);
3928 _updateSizeAndTransform();
3929 _schedulePeriodicPostFrameCallbacks();
3930 _textInputConnection!
3931 ..setStyle(
3932 fontFamily: _style.fontFamily,
3933 fontSize: _style.fontSize,
3934 fontWeight: _style.fontWeight,
3935 textDirection: _textDirection,
3936 textAlign: widget.textAlign,
3937 )
3938 ..setEditingState(localValue)
3939 ..show();
3940 if (_needsAutofill) {
3941 // Request autofill AFTER the size and the transform have been sent to
3942 // the platform text input plugin.
3943 _textInputConnection!.requestAutofill();
3944 }
3945 _lastKnownRemoteTextEditingValue = localValue;
3946 } else {
3947 _textInputConnection!.show();
3948 }
3949 }
3950
3951 void _closeInputConnectionIfNeeded() {
3952 if (_hasInputConnection) {
3953 _textInputConnection!.close();
3954 _textInputConnection = null;
3955 _lastKnownRemoteTextEditingValue = null;
3956 _scribbleCacheKey = null;
3957 removeTextPlaceholder();
3958 }
3959 }
3960
3961 void _openOrCloseInputConnectionIfNeeded() {
3962 if (_hasFocus && widget.focusNode.consumeKeyboardToken()) {
3963 _openInputConnection();
3964 } else if (!_hasFocus) {
3965 _closeInputConnectionIfNeeded();
3966 widget.controller.clearComposing();
3967 }
3968 }
3969
3970 bool _restartConnectionScheduled = false;
3971 void _scheduleRestartConnection() {
3972 if (_restartConnectionScheduled) {
3973 return;
3974 }
3975 _restartConnectionScheduled = true;
3976 scheduleMicrotask(_restartConnectionIfNeeded);
3977 }
3978
3979 // Discards the current [TextInputConnection] and establishes a new one.
3980 //
3981 // This method is rarely needed. This is currently used to reset the input
3982 // type when the "submit" text input action is triggered and the developer
3983 // puts the focus back to this input field..
3984 void _restartConnectionIfNeeded() {
3985 _restartConnectionScheduled = false;
3986 if (!_hasInputConnection || !_shouldCreateInputConnection) {
3987 return;
3988 }
3989 _textInputConnection!.close();
3990 _textInputConnection = null;
3991 _lastKnownRemoteTextEditingValue = null;
3992
3993 final AutofillScope? currentAutofillScope = _needsAutofill ? this.currentAutofillScope : null;
3994 final TextInputConnection newConnection =
3995 currentAutofillScope?.attach(this, textInputConfiguration) ??
3996 TextInput.attach(this, _effectiveAutofillClient.textInputConfiguration);
3997 _textInputConnection = newConnection;
3998
3999 newConnection
4000 ..show()
4001 ..setStyle(
4002 fontFamily: _style.fontFamily,
4003 fontSize: _style.fontSize,
4004 fontWeight: _style.fontWeight,
4005 textDirection: _textDirection,
4006 textAlign: widget.textAlign,
4007 )
4008 ..setEditingState(_value);
4009 _lastKnownRemoteTextEditingValue = _value;
4010 }
4011
4012 @override
4013 void didChangeInputControl(TextInputControl? oldControl, TextInputControl? newControl) {
4014 if (_hasFocus && _hasInputConnection) {
4015 oldControl?.hide();
4016 newControl?.show();
4017 }
4018 }
4019
4020 @override
4021 void connectionClosed() {
4022 if (_hasInputConnection) {
4023 _textInputConnection!.connectionClosedReceived();
4024 _textInputConnection = null;
4025 _lastKnownRemoteTextEditingValue = null;
4026 widget.focusNode.unfocus();
4027 }
4028 }
4029
4030 // Indicates that a call to _handleFocusChanged originated within
4031 // EditableText, allowing it to distinguish between internal and external
4032 // focus changes.
4033 bool _nextFocusChangeIsInternal = false;
4034
4035 // Sets _nextFocusChangeIsInternal to true only until any subsequent focus
4036 // change happens.
4037 void _flagInternalFocus() {
4038 _nextFocusChangeIsInternal = true;
4039 FocusManager.instance.addListener(_unflagInternalFocus);
4040 }
4041
4042 void _unflagInternalFocus() {
4043 _nextFocusChangeIsInternal = false;
4044 FocusManager.instance.removeListener(_unflagInternalFocus);
4045 }
4046
4047 /// Express interest in interacting with the keyboard.
4048 ///
4049 /// If this control is already attached to the keyboard, this function will
4050 /// request that the keyboard become visible. Otherwise, this function will
4051 /// ask the focus system that it become focused. If successful in acquiring
4052 /// focus, the control will then attach to the keyboard and request that the
4053 /// keyboard become visible.
4054 void requestKeyboard() {
4055 if (_hasFocus) {
4056 _openInputConnection();
4057 } else {
4058 _flagInternalFocus();
4059 widget.focusNode
4060 .requestFocus(); // This eventually calls _openInputConnection also, see _handleFocusChanged.
4061 }
4062 }
4063
4064 void _updateOrDisposeSelectionOverlayIfNeeded() {
4065 if (_selectionOverlay != null) {
4066 if (_hasFocus) {
4067 _selectionOverlay!.update(_value);
4068 } else {
4069 _selectionOverlay!.dispose();
4070 _selectionOverlay = null;
4071 }
4072 }
4073 }
4074
4075 final bool _platformSupportsFadeOnScroll = switch (defaultTargetPlatform) {
4076 TargetPlatform.android || TargetPlatform.iOS => true,
4077 TargetPlatform.fuchsia ||
4078 TargetPlatform.linux ||
4079 TargetPlatform.macOS ||
4080 TargetPlatform.windows => false,
4081 };
4082
4083 bool _isInternalScrollableNotification(BuildContext? notificationContext) {
4084 final ScrollableState? scrollableState = notificationContext
4085 ?.findAncestorStateOfType<ScrollableState>();
4086 return _scrollableKey.currentContext == scrollableState?.context;
4087 }
4088
4089 bool _scrollableNotificationIsFromSameSubtree(BuildContext? notificationContext) {
4090 if (notificationContext == null) {
4091 return false;
4092 }
4093 BuildContext? currentContext = context;
4094 // The notification context of a ScrollNotification points to the RawGestureDetector
4095 // of the Scrollable. We get the ScrollableState associated with this notification
4096 // by looking up the tree.
4097 final ScrollableState? notificationScrollableState = notificationContext
4098 .findAncestorStateOfType<ScrollableState>();
4099 if (notificationScrollableState == null) {
4100 return false;
4101 }
4102 while (currentContext != null) {
4103 final ScrollableState? scrollableState = currentContext
4104 .findAncestorStateOfType<ScrollableState>();
4105 if (scrollableState == notificationScrollableState) {
4106 return true;
4107 }
4108 currentContext = scrollableState?.context;
4109 }
4110 return false;
4111 }
4112
4113 void _handleContextMenuOnParentScroll(ScrollNotification notification) {
4114 // Do some preliminary checks to avoid expensive subtree traversal.
4115 if (notification is! ScrollStartNotification && notification is! ScrollEndNotification) {
4116 return;
4117 }
4118 switch (notification) {
4119 case ScrollStartNotification() when _dataWhenToolbarShowScheduled != null:
4120 case ScrollEndNotification() when _dataWhenToolbarShowScheduled == null:
4121 break;
4122 case ScrollEndNotification() when _dataWhenToolbarShowScheduled!.value != _value:
4123 _dataWhenToolbarShowScheduled = null;
4124 _disposeScrollNotificationObserver();
4125 case ScrollNotification(:final BuildContext? context)
4126 when !_isInternalScrollableNotification(context) &&
4127 _scrollableNotificationIsFromSameSubtree(context):
4128 _handleContextMenuOnScroll(notification);
4129 }
4130 }
4131
4132 Rect _calculateDeviceRect() {
4133 final Size screenSize = MediaQuery.sizeOf(context);
4134 final ui.FlutterView view = View.of(context);
4135 final double obscuredVertical =
4136 (view.padding.top + view.padding.bottom + view.viewInsets.bottom) / view.devicePixelRatio;
4137 final double obscuredHorizontal =
4138 (view.padding.left + view.padding.right) / view.devicePixelRatio;
4139 final Size visibleScreenSize = Size(
4140 screenSize.width - obscuredHorizontal,
4141 screenSize.height - obscuredVertical,
4142 );
4143 return Rect.fromLTWH(
4144 view.padding.left / view.devicePixelRatio,
4145 view.padding.top / view.devicePixelRatio,
4146 visibleScreenSize.width,
4147 visibleScreenSize.height,
4148 );
4149 }
4150
4151 bool _showToolbarOnScreenScheduled = false;
4152 void _handleContextMenuOnScroll(ScrollNotification notification) {
4153 if (_webContextMenuEnabled) {
4154 return;
4155 }
4156 if (!_platformSupportsFadeOnScroll) {
4157 _selectionOverlay?.updateForScroll();
4158 return;
4159 }
4160 // When the scroll begins and the toolbar is visible, hide it
4161 // until scrolling ends.
4162 //
4163 // The selection and renderEditable need to be visible within the current
4164 // viewport for the toolbar to show when scrolling ends. If they are not
4165 // then the toolbar is shown when they are scrolled back into view, unless
4166 // invalidated by a change in TextEditingValue.
4167 if (notification is ScrollStartNotification) {
4168 if (_dataWhenToolbarShowScheduled != null) {
4169 return;
4170 }
4171 final bool toolbarIsVisible =
4172 _selectionOverlay != null &&
4173 _selectionOverlay!.toolbarIsVisible &&
4174 !_selectionOverlay!.spellCheckToolbarIsVisible;
4175 if (!toolbarIsVisible) {
4176 return;
4177 }
4178 final List<TextBox> selectionBoxes = renderEditable.getBoxesForSelection(_value.selection);
4179 final Rect selectionBounds = _value.selection.isCollapsed || selectionBoxes.isEmpty
4180 ? renderEditable.getLocalRectForCaret(_value.selection.extent)
4181 : selectionBoxes
4182 .map((TextBox box) => box.toRect())
4183 .reduce((Rect result, Rect rect) => result.expandToInclude(rect));
4184 _dataWhenToolbarShowScheduled = (value: _value, selectionBounds: selectionBounds);
4185 _selectionOverlay?.hideToolbar();
4186 } else if (notification is ScrollEndNotification) {
4187 if (_dataWhenToolbarShowScheduled == null) {
4188 return;
4189 }
4190 if (_dataWhenToolbarShowScheduled!.value != _value) {
4191 // Value has changed so we should invalidate any toolbar scheduling.
4192 _dataWhenToolbarShowScheduled = null;
4193 _disposeScrollNotificationObserver();
4194 return;
4195 }
4196
4197 if (_showToolbarOnScreenScheduled) {
4198 return;
4199 }
4200 _showToolbarOnScreenScheduled = true;
4201 SchedulerBinding.instance.addPostFrameCallback((Duration _) {
4202 _showToolbarOnScreenScheduled = false;
4203 if (!mounted) {
4204 return;
4205 }
4206 final Rect deviceRect = _calculateDeviceRect();
4207 final bool selectionVisibleInEditable =
4208 renderEditable.selectionStartInViewport.value ||
4209 renderEditable.selectionEndInViewport.value;
4210 final Rect selectionBounds = MatrixUtils.transformRect(
4211 renderEditable.getTransformTo(null),
4212 _dataWhenToolbarShowScheduled!.selectionBounds,
4213 );
4214 final bool selectionOverlapsWithDeviceRect =
4215 !selectionBounds.hasNaN && deviceRect.overlaps(selectionBounds);
4216
4217 if (selectionVisibleInEditable &&
4218 selectionOverlapsWithDeviceRect &&
4219 _selectionInViewport(_dataWhenToolbarShowScheduled!.selectionBounds)) {
4220 showToolbar();
4221 _dataWhenToolbarShowScheduled = null;
4222 }
4223 }, debugLabel: 'EditableText.scheduleToolbar');
4224 }
4225 }
4226
4227 bool _selectionInViewport(Rect selectionBounds) {
4228 RenderAbstractViewport? closestViewport = RenderAbstractViewport.maybeOf(renderEditable);
4229 while (closestViewport != null) {
4230 final Rect selectionBoundsLocalToViewport = MatrixUtils.transformRect(
4231 renderEditable.getTransformTo(closestViewport),
4232 selectionBounds,
4233 );
4234 if (selectionBoundsLocalToViewport.hasNaN ||
4235 closestViewport.paintBounds.hasNaN ||
4236 !closestViewport.paintBounds.overlaps(selectionBoundsLocalToViewport)) {
4237 return false;
4238 }
4239 closestViewport = RenderAbstractViewport.maybeOf(closestViewport.parent);
4240 }
4241 return true;
4242 }
4243
4244 TextSelectionOverlay _createSelectionOverlay() {
4245 final EditableTextContextMenuBuilder? contextMenuBuilder = widget.contextMenuBuilder;
4246 final TextSelectionOverlay selectionOverlay = TextSelectionOverlay(
4247 clipboardStatus: clipboardStatus,
4248 context: context,
4249 value: _value,
4250 debugRequiredFor: widget,
4251 toolbarLayerLink: _toolbarLayerLink,
4252 startHandleLayerLink: _startHandleLayerLink,
4253 endHandleLayerLink: _endHandleLayerLink,
4254 renderObject: renderEditable,
4255 selectionControls: widget.selectionControls,
4256 selectionDelegate: this,
4257 dragStartBehavior: widget.dragStartBehavior,
4258 onSelectionHandleTapped: widget.onSelectionHandleTapped,
4259 contextMenuBuilder: contextMenuBuilder == null || _webContextMenuEnabled
4260 ? null
4261 : (BuildContext context) {
4262 return contextMenuBuilder(context, this);
4263 },
4264 magnifierConfiguration: widget.magnifierConfiguration,
4265 );
4266
4267 return selectionOverlay;
4268 }
4269
4270 @pragma('vm:notify-debugger-on-exception')
4271 void _handleSelectionChanged(TextSelection selection, SelectionChangedCause? cause) {
4272 // We return early if the selection is not valid. This can happen when the
4273 // text of [EditableText] is updated at the same time as the selection is
4274 // changed by a gesture event.
4275 final String text = widget.controller.value.text;
4276 if (text.length < selection.end || text.length < selection.start) {
4277 return;
4278 }
4279
4280 widget.controller.selection = selection;
4281
4282 // This will show the keyboard for all selection changes on the
4283 // EditableText except for those triggered by a keyboard input.
4284 // Typically EditableText shouldn't take user keyboard input if
4285 // it's not focused already. If the EditableText is being
4286 // autofilled it shouldn't request focus.
4287 switch (cause) {
4288 case null:
4289 case SelectionChangedCause.doubleTap:
4290 case SelectionChangedCause.drag:
4291 case SelectionChangedCause.forcePress:
4292 case SelectionChangedCause.longPress:
4293 case SelectionChangedCause.stylusHandwriting:
4294 case SelectionChangedCause.tap:
4295 case SelectionChangedCause.toolbar:
4296 requestKeyboard();
4297 case SelectionChangedCause.keyboard:
4298 }
4299 if (widget.selectionControls == null && widget.contextMenuBuilder == null) {
4300 _selectionOverlay?.dispose();
4301 _selectionOverlay = null;
4302 } else {
4303 if (_selectionOverlay == null) {
4304 _selectionOverlay = _createSelectionOverlay();
4305 } else {
4306 _selectionOverlay!.update(_value);
4307 }
4308 _selectionOverlay!.handlesVisible = widget.showSelectionHandles;
4309 _selectionOverlay!.showHandles();
4310 }
4311 // TODO(chunhtai): we should make sure selection actually changed before
4312 // we call the onSelectionChanged.
4313 // https://github.com/flutter/flutter/issues/76349.
4314 try {
4315 widget.onSelectionChanged?.call(selection, cause);
4316 } catch (exception, stack) {
4317 FlutterError.reportError(
4318 FlutterErrorDetails(
4319 exception: exception,
4320 stack: stack,
4321 library: 'widgets',
4322 context: ErrorDescription('while calling onSelectionChanged for $cause'),
4323 ),
4324 );
4325 }
4326
4327 // To keep the cursor from blinking while it moves, restart the timer here.
4328 if (_showBlinkingCursor && _cursorTimer != null) {
4329 _stopCursorBlink(resetCharTicks: false);
4330 _startCursorBlink();
4331 }
4332 }
4333
4334 // Animation configuration for scrolling the caret back on screen.
4335 static const Duration _caretAnimationDuration = Duration(milliseconds: 100);
4336 static const Curve _caretAnimationCurve = Curves.fastOutSlowIn;
4337
4338 bool _showCaretOnScreenScheduled = false;
4339
4340 void _scheduleShowCaretOnScreen({required bool withAnimation}) {
4341 if (_showCaretOnScreenScheduled) {
4342 return;
4343 }
4344 _showCaretOnScreenScheduled = true;
4345 SchedulerBinding.instance.addPostFrameCallback((Duration _) {
4346 _showCaretOnScreenScheduled = false;
4347 // Since we are in a post frame callback, check currentContext in case
4348 // RenderEditable has been disposed (in which case it will be null).
4349 final RenderEditable? renderEditable =
4350 _editableKey.currentContext?.findRenderObject() as RenderEditable?;
4351 if (renderEditable == null ||
4352 !(renderEditable.selection?.isValid ?? false) ||
4353 !_scrollController.hasClients) {
4354 return;
4355 }
4356
4357 final double lineHeight = renderEditable.preferredLineHeight;
4358
4359 // Enlarge the target rect by scrollPadding to ensure that caret is not
4360 // positioned directly at the edge after scrolling.
4361 double bottomSpacing = widget.scrollPadding.bottom;
4362 if (_selectionOverlay?.selectionControls != null) {
4363 final double handleHeight = _selectionOverlay!.selectionControls!
4364 .getHandleSize(lineHeight)
4365 .height;
4366 final double interactiveHandleHeight = math.max(handleHeight, kMinInteractiveDimension);
4367 final Offset anchor = _selectionOverlay!.selectionControls!.getHandleAnchor(
4368 TextSelectionHandleType.collapsed,
4369 lineHeight,
4370 );
4371 final double handleCenter = handleHeight / 2 - anchor.dy;
4372 bottomSpacing = math.max(handleCenter + interactiveHandleHeight / 2, bottomSpacing);
4373 }
4374
4375 final EdgeInsets caretPadding = widget.scrollPadding.copyWith(bottom: bottomSpacing);
4376
4377 final Rect caretRect = renderEditable.getLocalRectForCaret(renderEditable.selection!.extent);
4378 final RevealedOffset targetOffset = _getOffsetToRevealCaret(caretRect);
4379
4380 final Rect rectToReveal;
4381 final TextSelection selection = textEditingValue.selection;
4382 if (selection.isCollapsed) {
4383 rectToReveal = targetOffset.rect;
4384 } else {
4385 final List<TextBox> selectionBoxes = renderEditable.getBoxesForSelection(selection);
4386 // selectionBoxes may be empty if, for example, the selection does not
4387 // encompass a full character, like if it only contained part of an
4388 // extended grapheme cluster.
4389 if (selectionBoxes.isEmpty) {
4390 rectToReveal = targetOffset.rect;
4391 } else {
4392 rectToReveal = selection.baseOffset < selection.extentOffset
4393 ? selectionBoxes.last.toRect()
4394 : selectionBoxes.first.toRect();
4395 }
4396 }
4397
4398 if (withAnimation) {
4399 _scrollController.animateTo(
4400 targetOffset.offset,
4401 duration: _caretAnimationDuration,
4402 curve: _caretAnimationCurve,
4403 );
4404 renderEditable.showOnScreen(
4405 rect: caretPadding.inflateRect(rectToReveal),
4406 duration: _caretAnimationDuration,
4407 curve: _caretAnimationCurve,
4408 );
4409 } else {
4410 _scrollController.jumpTo(targetOffset.offset);
4411 renderEditable.showOnScreen(rect: caretPadding.inflateRect(rectToReveal));
4412 }
4413 }, debugLabel: 'EditableText.showCaret');
4414 }
4415
4416 late double _lastBottomViewInset;
4417
4418 @override
4419 void didChangeMetrics() {
4420 if (!mounted) {
4421 return;
4422 }
4423 final ui.FlutterView view = View.of(context);
4424 if (_lastBottomViewInset != view.viewInsets.bottom) {
4425 SchedulerBinding.instance.addPostFrameCallback((Duration _) {
4426 _selectionOverlay?.updateForScroll();
4427 }, debugLabel: 'EditableText.updateForScroll');
4428 if (_lastBottomViewInset < view.viewInsets.bottom) {
4429 // Because the metrics change signal from engine will come here every frame
4430 // (on both iOS and Android). So we don't need to show caret with animation.
4431 _scheduleShowCaretOnScreen(withAnimation: false);
4432 }
4433 }
4434 _lastBottomViewInset = view.viewInsets.bottom;
4435 }
4436
4437 Future<void> _performSpellCheck(final String text) async {
4438 try {
4439 final Locale? localeForSpellChecking = widget.locale ?? Localizations.maybeLocaleOf(context);
4440
4441 assert(
4442 localeForSpellChecking != null,
4443 'Locale must be specified in widget or Localization widget must be in scope',
4444 );
4445
4446 final List<SuggestionSpan>? suggestions = await _spellCheckConfiguration.spellCheckService!
4447 .fetchSpellCheckSuggestions(localeForSpellChecking!, text);
4448
4449 if (suggestions == null) {
4450 // The request to fetch spell check suggestions was canceled due to ongoing request.
4451 return;
4452 }
4453
4454 spellCheckResults = SpellCheckResults(text, suggestions);
4455 renderEditable.text = buildTextSpan();
4456 } catch (exception, stack) {
4457 FlutterError.reportError(
4458 FlutterErrorDetails(
4459 exception: exception,
4460 stack: stack,
4461 library: 'widgets',
4462 context: ErrorDescription('while performing spell check'),
4463 ),
4464 );
4465 }
4466 }
4467
4468 @pragma('vm:notify-debugger-on-exception')
4469 void _formatAndSetValue(
4470 TextEditingValue value,
4471 SelectionChangedCause? cause, {
4472 bool userInteraction = false,
4473 }) {
4474 final TextEditingValue oldValue = _value;
4475 final bool textChanged = oldValue.text != value.text;
4476 final bool textCommitted = !oldValue.composing.isCollapsed && value.composing.isCollapsed;
4477 final bool selectionChanged = oldValue.selection != value.selection;
4478
4479 if (textChanged || textCommitted) {
4480 // Only apply input formatters if the text has changed (including uncommitted
4481 // text in the composing region), or when the user committed the composing
4482 // text.
4483 // Gboard is very persistent in restoring the composing region. Applying
4484 // input formatters on composing-region-only changes (except clearing the
4485 // current composing region) is very infinite-loop-prone: the formatters
4486 // will keep trying to modify the composing region while Gboard will keep
4487 // trying to restore the original composing region.
4488 try {
4489 value =
4490 widget.inputFormatters?.fold<TextEditingValue>(
4491 value,
4492 (TextEditingValue newValue, TextInputFormatter formatter) =>
4493 formatter.formatEditUpdate(_value, newValue),
4494 ) ??
4495 value;
4496
4497 if (spellCheckEnabled && value.text.isNotEmpty && _value.text != value.text) {
4498 _performSpellCheck(value.text);
4499 }
4500 } catch (exception, stack) {
4501 FlutterError.reportError(
4502 FlutterErrorDetails(
4503 exception: exception,
4504 stack: stack,
4505 library: 'widgets',
4506 context: ErrorDescription('while applying input formatters'),
4507 ),
4508 );
4509 }
4510 }
4511
4512 final TextSelection oldTextSelection = textEditingValue.selection;
4513
4514 // Put all optional user callback invocations in a batch edit to prevent
4515 // sending multiple `TextInput.updateEditingValue` messages.
4516 beginBatchEdit();
4517 _value = value;
4518 // Changes made by the keyboard can sometimes be "out of band" for listening
4519 // components, so always send those events, even if we didn't think it
4520 // changed. Also, the user long pressing should always send a selection change
4521 // as well.
4522 if (selectionChanged ||
4523 (userInteraction &&
4524 (cause == SelectionChangedCause.longPress ||
4525 cause == SelectionChangedCause.keyboard))) {
4526 _handleSelectionChanged(_value.selection, cause);
4527 _bringIntoViewBySelectionState(oldTextSelection, value.selection, cause);
4528 }
4529 final String currentText = _value.text;
4530 if (oldValue.text != currentText) {
4531 try {
4532 widget.onChanged?.call(currentText);
4533 } catch (exception, stack) {
4534 FlutterError.reportError(
4535 FlutterErrorDetails(
4536 exception: exception,
4537 stack: stack,
4538 library: 'widgets',
4539 context: ErrorDescription('while calling onChanged'),
4540 ),
4541 );
4542 }
4543 }
4544 endBatchEdit();
4545 }
4546
4547 void _bringIntoViewBySelectionState(
4548 TextSelection oldSelection,
4549 TextSelection newSelection,
4550 SelectionChangedCause? cause,
4551 ) {
4552 switch (defaultTargetPlatform) {
4553 case TargetPlatform.iOS:
4554 case TargetPlatform.macOS:
4555 if (cause == SelectionChangedCause.longPress || cause == SelectionChangedCause.drag) {
4556 bringIntoView(newSelection.extent);
4557 }
4558 case TargetPlatform.linux:
4559 case TargetPlatform.windows:
4560 case TargetPlatform.fuchsia:
4561 case TargetPlatform.android:
4562 if (cause == SelectionChangedCause.drag) {
4563 if (oldSelection.baseOffset != newSelection.baseOffset) {
4564 bringIntoView(newSelection.base);
4565 } else if (oldSelection.extentOffset != newSelection.extentOffset) {
4566 bringIntoView(newSelection.extent);
4567 }
4568 }
4569 }
4570 }
4571
4572 void _onCursorColorTick() {
4573 final double effectiveOpacity = math.min(
4574 widget.cursorColor.alpha / 255.0,
4575 _cursorBlinkOpacityController.value,
4576 );
4577 renderEditable.cursorColor = widget.cursorColor.withOpacity(effectiveOpacity);
4578 _cursorVisibilityNotifier.value =
4579 widget.showCursor &&
4580 (EditableText.debugDeterministicCursor || _cursorBlinkOpacityController.value > 0);
4581 }
4582
4583 bool get _showBlinkingCursor =>
4584 _hasFocus &&
4585 _value.selection.isCollapsed &&
4586 widget.showCursor &&
4587 _tickersEnabled &&
4588 !renderEditable.floatingCursorOn;
4589
4590 /// Whether the blinking cursor is actually visible at this precise moment
4591 /// (it's hidden half the time, since it blinks).
4592 @visibleForTesting
4593 bool get cursorCurrentlyVisible => _cursorBlinkOpacityController.value > 0;
4594
4595 /// The cursor blink interval (the amount of time the cursor is in the "on"
4596 /// state or the "off" state). A complete cursor blink period is twice this
4597 /// value (half on, half off).
4598 @visibleForTesting
4599 Duration get cursorBlinkInterval => _kCursorBlinkHalfPeriod;
4600
4601 /// The current status of the text selection handles.
4602 @visibleForTesting
4603 TextSelectionOverlay? get selectionOverlay => _selectionOverlay;
4604
4605 int _obscureShowCharTicksPending = 0;
4606 int? _obscureLatestCharIndex;
4607
4608 void _startCursorBlink() {
4609 assert(
4610 !(_cursorTimer?.isActive ?? false) ||
4611 !(_backingCursorBlinkOpacityController?.isAnimating ?? false),
4612 );
4613 if (!widget.showCursor) {
4614 return;
4615 }
4616 if (!_tickersEnabled) {
4617 return;
4618 }
4619 _cursorTimer?.cancel();
4620 _cursorBlinkOpacityController.value = 1.0;
4621 if (EditableText.debugDeterministicCursor) {
4622 return;
4623 }
4624 if (widget.cursorOpacityAnimates) {
4625 _cursorBlinkOpacityController
4626 .animateWith(_iosBlinkCursorSimulation)
4627 .whenComplete(_onCursorTick);
4628 } else {
4629 _cursorTimer = Timer.periodic(_kCursorBlinkHalfPeriod, (Timer timer) {
4630 _onCursorTick();
4631 });
4632 }
4633 }
4634
4635 void _onCursorTick() {
4636 if (_obscureShowCharTicksPending > 0) {
4637 _obscureShowCharTicksPending = WidgetsBinding.instance.platformDispatcher.brieflyShowPassword
4638 ? _obscureShowCharTicksPending - 1
4639 : 0;
4640 if (_obscureShowCharTicksPending == 0) {
4641 setState(() {});
4642 }
4643 }
4644
4645 if (widget.cursorOpacityAnimates) {
4646 _cursorTimer?.cancel();
4647 // Schedule this as an async task to avoid blocking tester.pumpAndSettle
4648 // indefinitely.
4649 _cursorTimer = Timer(
4650 Duration.zero,
4651 () => _cursorBlinkOpacityController
4652 .animateWith(_iosBlinkCursorSimulation)
4653 .whenComplete(_onCursorTick),
4654 );
4655 } else {
4656 if (!(_cursorTimer?.isActive ?? false) && _tickersEnabled) {
4657 _cursorTimer = Timer.periodic(_kCursorBlinkHalfPeriod, (Timer timer) {
4658 _onCursorTick();
4659 });
4660 }
4661 _cursorBlinkOpacityController.value = _cursorBlinkOpacityController.value == 0 ? 1 : 0;
4662 }
4663 }
4664
4665 void _stopCursorBlink({bool resetCharTicks = true}) {
4666 // If the cursor is animating, stop the animation, and we always
4667 // want the cursor to be visible when the floating cursor is enabled.
4668 _cursorBlinkOpacityController.value = renderEditable.floatingCursorOn ? 1.0 : 0.0;
4669 _cursorTimer?.cancel();
4670 _cursorTimer = null;
4671 if (resetCharTicks) {
4672 _obscureShowCharTicksPending = 0;
4673 }
4674 }
4675
4676 void _startOrStopCursorTimerIfNeeded() {
4677 if (!_showBlinkingCursor) {
4678 _stopCursorBlink();
4679 } else if (_cursorTimer == null) {
4680 _startCursorBlink();
4681 }
4682 }
4683
4684 void _didChangeTextEditingValue() {
4685 if (_hasFocus && !_value.selection.isValid) {
4686 // If this field is focused and the selection is invalid, place the cursor at
4687 // the end. Does not rely on _handleFocusChanged because it makes selection
4688 // handles visible on Android.
4689 // Unregister as a listener to the text controller while making the change.
4690 widget.controller.removeListener(_didChangeTextEditingValue);
4691 widget.controller.selection = _adjustedSelectionWhenFocused()!;
4692 widget.controller.addListener(_didChangeTextEditingValue);
4693 }
4694 _updateRemoteEditingValueIfNeeded();
4695 _startOrStopCursorTimerIfNeeded();
4696 _updateOrDisposeSelectionOverlayIfNeeded();
4697 // TODO(abarth): Teach RenderEditable about ValueNotifier
4698 // to avoid this setState().
4699 setState(() {
4700 /* We use widget.controller.value in build(). */
4701 });
4702 _verticalSelectionUpdateAction.stopCurrentVerticalRunIfSelectionChanges();
4703 }
4704
4705 void _handleFocusChanged() {
4706 _openOrCloseInputConnectionIfNeeded();
4707 _startOrStopCursorTimerIfNeeded();
4708 _updateOrDisposeSelectionOverlayIfNeeded();
4709 if (_hasFocus) {
4710 // Listen for changing viewInsets, which indicates keyboard showing up.
4711 WidgetsBinding.instance.addObserver(this);
4712 _lastBottomViewInset = View.of(context).viewInsets.bottom;
4713 if (!widget.readOnly) {
4714 _scheduleShowCaretOnScreen(withAnimation: true);
4715 }
4716 final TextSelection? updatedSelection = _adjustedSelectionWhenFocused();
4717 if (updatedSelection != null) {
4718 _handleSelectionChanged(updatedSelection, null);
4719 }
4720 } else {
4721 WidgetsBinding.instance.removeObserver(this);
4722 setState(() {
4723 _currentPromptRectRange = null;
4724 });
4725 }
4726 updateKeepAlive();
4727 }
4728
4729 TextSelection? _adjustedSelectionWhenFocused() {
4730 TextSelection? selection;
4731 final bool shouldSelectAll =
4732 widget.selectAllOnFocus &&
4733 widget.selectionEnabled &&
4734 !_isMultiline &&
4735 !_nextFocusChangeIsInternal &&
4736 !_justResumed;
4737 _justResumed = false;
4738 if (shouldSelectAll) {
4739 // On native web and desktop platforms, single line tags
4740 // select all when receiving focus.
4741 selection = TextSelection(baseOffset: 0, extentOffset: _value.text.length);
4742 } else if (!_value.selection.isValid) {
4743 // Place cursor at the end if the selection is invalid when we receive focus.
4744 selection = TextSelection.collapsed(offset: _value.text.length);
4745 }
4746 return selection;
4747 }
4748
4749 void _compositeCallback(Layer layer) {
4750 // The callback can be invoked when the layer is detached.
4751 // The input connection can be closed by the platform in which case this
4752 // widget doesn't rebuild.
4753 if (!renderEditable.attached || !_hasInputConnection) {
4754 return;
4755 }
4756 assert(mounted);
4757 assert((context as Element).debugIsActive);
4758 _updateSizeAndTransform();
4759 }
4760
4761 // Must be called after layout.
4762 // See https://github.com/flutter/flutter/issues/126312
4763 void _updateSizeAndTransform() {
4764 final Size size = renderEditable.size;
4765 final Matrix4 transform = renderEditable.getTransformTo(null);
4766 _textInputConnection!.setEditableSizeAndTransform(size, transform);
4767 }
4768
4769 void _schedulePeriodicPostFrameCallbacks([Duration? duration]) {
4770 if (!_hasInputConnection) {
4771 return;
4772 }
4773 _updateSelectionRects();
4774 _updateComposingRectIfNeeded();
4775 _updateCaretRectIfNeeded();
4776 SchedulerBinding.instance.addPostFrameCallback(
4777 _schedulePeriodicPostFrameCallbacks,
4778 debugLabel: 'EditableText.postFrameCallbacks',
4779 );
4780 }
4781
4782 _ScribbleCacheKey? _scribbleCacheKey;
4783
4784 void _updateSelectionRects({bool force = false}) {
4785 if (!_stylusHandwritingEnabled || defaultTargetPlatform != TargetPlatform.iOS) {
4786 return;
4787 }
4788
4789 final ScrollDirection scrollDirection = _scrollController.position.userScrollDirection;
4790 if (scrollDirection != ScrollDirection.idle) {
4791 return;
4792 }
4793
4794 final InlineSpan inlineSpan = renderEditable.text!;
4795 final TextScaler effectiveTextScaler = switch ((widget.textScaler, widget.textScaleFactor)) {
4796 (final TextScaler textScaler, _) => textScaler,
4797 (null, final double textScaleFactor) => TextScaler.linear(textScaleFactor),
4798 (null, null) => MediaQuery.textScalerOf(context),
4799 };
4800
4801 final _ScribbleCacheKey newCacheKey = _ScribbleCacheKey(
4802 inlineSpan: inlineSpan,
4803 textAlign: widget.textAlign,
4804 textDirection: _textDirection,
4805 textScaler: effectiveTextScaler,
4806 textHeightBehavior: widget.textHeightBehavior ?? DefaultTextHeightBehavior.maybeOf(context),
4807 locale: widget.locale,
4808 structStyle: widget.strutStyle,
4809 placeholder: _placeholderLocation,
4810 size: renderEditable.size,
4811 );
4812
4813 final RenderComparison comparison = force
4814 ? RenderComparison.layout
4815 : _scribbleCacheKey?.compare(newCacheKey) ?? RenderComparison.layout;
4816 if (comparison.index < RenderComparison.layout.index) {
4817 return;
4818 }
4819 _scribbleCacheKey = newCacheKey;
4820
4821 final List<SelectionRect> rects = <SelectionRect>[];
4822 int graphemeStart = 0;
4823 // Can't use _value.text here: the controller value could change between
4824 // frames.
4825 final String plainText = inlineSpan.toPlainText(includeSemanticsLabels: false);
4826 final CharacterRange characterRange = CharacterRange(plainText);
4827 while (characterRange.moveNext()) {
4828 final int graphemeEnd = graphemeStart + characterRange.current.length;
4829 final List<TextBox> boxes = renderEditable.getBoxesForSelection(
4830 TextSelection(baseOffset: graphemeStart, extentOffset: graphemeEnd),
4831 );
4832
4833 final TextBox? box = boxes.isEmpty ? null : boxes.first;
4834 if (box != null) {
4835 final Rect paintBounds = renderEditable.paintBounds;
4836 // Stop early when characters are already below the bottom edge of the
4837 // RenderEditable, regardless of its clipBehavior.
4838 if (paintBounds.bottom <= box.top) {
4839 break;
4840 }
4841 // Include any TextBox which intersects with the RenderEditable.
4842 if (paintBounds.left <= box.right &&
4843 box.left <= paintBounds.right &&
4844 paintBounds.top <= box.bottom) {
4845 // At least some part of the letter is visible within the text field.
4846 rects.add(
4847 SelectionRect(position: graphemeStart, bounds: box.toRect(), direction: box.direction),
4848 );
4849 }
4850 }
4851 graphemeStart = graphemeEnd;
4852 }
4853 _textInputConnection!.setSelectionRects(rects);
4854 }
4855
4856 // Sends the current composing rect to the embedder's text input plugin.
4857 //
4858 // In cases where the composing rect hasn't been updated in the embedder due
4859 // to the lag of asynchronous messages over the channel, the position of the
4860 // current caret rect is used instead.
4861 //
4862 // See: [_updateCaretRectIfNeeded]
4863 void _updateComposingRectIfNeeded() {
4864 final TextRange composingRange = _value.composing;
4865 assert(mounted);
4866 Rect? composingRect = renderEditable.getRectForComposingRange(composingRange);
4867 // Send the caret location instead if there's no marked text yet.
4868 if (composingRect == null) {
4869 final int offset = composingRange.isValid ? composingRange.start : 0;
4870 composingRect = renderEditable.getLocalRectForCaret(TextPosition(offset: offset));
4871 }
4872 _textInputConnection!.setComposingRect(composingRect);
4873 }
4874
4875 // Sends the current caret rect to the embedder's text input plugin.
4876 //
4877 // The position of the caret rect is updated periodically such that if the
4878 // user initiates composing input, the current cursor rect can be used for
4879 // the first character until the composing rect can be sent.
4880 //
4881 // On selection changes, the start of the selection is used. This ensures
4882 // that regardless of the direction the selection was created, the cursor is
4883 // set to the position where next text input occurs. This position is used to
4884 // position the IME's candidate selection menu.
4885 //
4886 // See: [_updateComposingRectIfNeeded]
4887 void _updateCaretRectIfNeeded() {
4888 final TextSelection? selection = renderEditable.selection;
4889 if (selection == null || !selection.isValid) {
4890 return;
4891 }
4892 final TextPosition currentTextPosition = TextPosition(offset: selection.start);
4893 final Rect caretRect = renderEditable.getLocalRectForCaret(currentTextPosition);
4894 _textInputConnection!.setCaretRect(caretRect);
4895 }
4896
4897 TextDirection get _textDirection => widget.textDirection ?? Directionality.of(context);
4898
4899 /// The renderer for this widget's descendant.
4900 ///
4901 /// This property is typically used to notify the renderer of input gestures
4902 /// when [RenderEditable.ignorePointer] is true.
4903 late final RenderEditable renderEditable =
4904 _editableKey.currentContext!.findRenderObject()! as RenderEditable;
4905
4906 @override
4907 TextEditingValue get textEditingValue => _value;
4908
4909 double get _devicePixelRatio => MediaQuery.devicePixelRatioOf(context);
4910
4911 @override
4912 void userUpdateTextEditingValue(TextEditingValue value, SelectionChangedCause? cause) {
4913 // Compare the current TextEditingValue with the pre-format new
4914 // TextEditingValue value, in case the formatter would reject the change.
4915 final bool shouldShowCaret = widget.readOnly
4916 ? _value.selection != value.selection
4917 : _value != value;
4918 if (shouldShowCaret) {
4919 _scheduleShowCaretOnScreen(withAnimation: true);
4920 }
4921
4922 // Even if the value doesn't change, it may be necessary to focus and build
4923 // the selection overlay. For example, this happens when right clicking an
4924 // unfocused field that previously had a selection in the same spot.
4925 if (value == textEditingValue) {
4926 if (!widget.focusNode.hasFocus) {
4927 _flagInternalFocus();
4928 widget.focusNode.requestFocus();
4929 _selectionOverlay ??= _createSelectionOverlay();
4930 }
4931 return;
4932 }
4933
4934 _formatAndSetValue(value, cause, userInteraction: true);
4935 }
4936
4937 @override
4938 void bringIntoView(TextPosition position) {
4939 final Rect localRect = renderEditable.getLocalRectForCaret(position);
4940 final RevealedOffset targetOffset = _getOffsetToRevealCaret(localRect);
4941
4942 _scrollController.jumpTo(targetOffset.offset);
4943 renderEditable.showOnScreen(rect: targetOffset.rect);
4944 }
4945
4946 /// Shows the selection toolbar at the location of the current cursor.
4947 ///
4948 /// Returns `false` if a toolbar couldn't be shown, such as when the toolbar
4949 /// is already shown, or when no text selection currently exists.
4950 @override
4951 bool showToolbar() {
4952 // Web is using native dom elements to enable clipboard functionality of the
4953 // context menu: copy, paste, select, cut. It might also provide additional
4954 // functionality depending on the browser (such as translate). Due to this,
4955 // we should not show a Flutter toolbar for the editable text elements
4956 // unless the browser's context menu is explicitly disabled.
4957 if (_webContextMenuEnabled) {
4958 return false;
4959 }
4960
4961 if (_selectionOverlay == null) {
4962 return false;
4963 }
4964 if (_selectionOverlay!.toolbarIsVisible) {
4965 return false;
4966 }
4967 _liveTextInputStatus?.update();
4968 clipboardStatus.update();
4969 _selectionOverlay!.showToolbar();
4970 // Listen to parent scroll events when the toolbar is visible so it can be
4971 // hidden during a scroll on supported platforms.
4972 if (_platformSupportsFadeOnScroll) {
4973 _listeningToScrollNotificationObserver = true;
4974 _scrollNotificationObserver?.removeListener(_handleContextMenuOnParentScroll);
4975 _scrollNotificationObserver = ScrollNotificationObserver.maybeOf(context);
4976 _scrollNotificationObserver?.addListener(_handleContextMenuOnParentScroll);
4977 }
4978 return true;
4979 }
4980
4981 @override
4982 void hideToolbar([bool hideHandles = true]) {
4983 // Stop listening to parent scroll events when toolbar is hidden.
4984 _disposeScrollNotificationObserver();
4985 if (hideHandles) {
4986 // Hide the handles and the toolbar.
4987 _selectionOverlay?.hide();
4988 } else if (_selectionOverlay?.toolbarIsVisible ?? false) {
4989 // Hide only the toolbar but not the handles.
4990 _selectionOverlay?.hideToolbar();
4991 }
4992 }
4993
4994 /// Toggles the visibility of the toolbar.
4995 void toggleToolbar([bool hideHandles = true]) {
4996 final TextSelectionOverlay selectionOverlay = _selectionOverlay ??= _createSelectionOverlay();
4997 if (selectionOverlay.toolbarIsVisible) {
4998 hideToolbar(hideHandles);
4999 } else {
5000 showToolbar();
5001 }
5002 }
5003
5004 /// Shows toolbar with spell check suggestions of misspelled words that are
5005 /// available for click-and-replace.
5006 bool showSpellCheckSuggestionsToolbar() {
5007 // Spell check suggestions toolbars are intended to be shown on non-web
5008 // platforms. Additionally, the Cupertino style toolbar can't be drawn on
5009 // the web with the HTML renderer due to
5010 // https://github.com/flutter/flutter/issues/123560.
5011 if (!spellCheckEnabled ||
5012 _webContextMenuEnabled ||
5013 widget.readOnly ||
5014 _selectionOverlay == null ||
5015 !_spellCheckResultsReceived ||
5016 findSuggestionSpanAtCursorIndex(textEditingValue.selection.extentOffset) == null) {
5017 // Only attempt to show the spell check suggestions toolbar if there
5018 // is a toolbar specified and spell check suggestions available to show.
5019 return false;
5020 }
5021
5022 assert(
5023 _spellCheckConfiguration.spellCheckSuggestionsToolbarBuilder != null,
5024 'spellCheckSuggestionsToolbarBuilder must be defined in '
5025 'SpellCheckConfiguration to show a toolbar with spell check '
5026 'suggestions',
5027 );
5028
5029 _selectionOverlay!.showSpellCheckSuggestionsToolbar((BuildContext context) {
5030 return _spellCheckConfiguration.spellCheckSuggestionsToolbarBuilder!(context, this);
5031 });
5032 return true;
5033 }
5034
5035 /// Shows the magnifier at the position given by `positionToShow`,
5036 /// if no magnifier exists.
5037 ///
5038 /// Updates the magnifier to the position given by `positionToShow`,
5039 /// if a magnifier exits.
5040 ///
5041 /// Does nothing if a magnifier couldn't be shown, such as when the selection
5042 /// overlay does not currently exist.
5043 void showMagnifier(Offset positionToShow) {
5044 if (_selectionOverlay == null) {
5045 return;
5046 }
5047
5048 if (_selectionOverlay!.magnifierExists) {
5049 _selectionOverlay!.updateMagnifier(positionToShow);
5050 } else {
5051 _selectionOverlay!.showMagnifier(positionToShow);
5052 }
5053 }
5054
5055 /// Hides the magnifier.
5056 void hideMagnifier() {
5057 if (_selectionOverlay == null) {
5058 return;
5059 }
5060
5061 _selectionOverlay!.hideMagnifier();
5062 }
5063
5064 // Tracks the location a [_ScribblePlaceholder] should be rendered in the
5065 // text.
5066 //
5067 // A value of -1 indicates there should be no placeholder, otherwise the
5068 // value should be between 0 and the length of the text, inclusive.
5069 int _placeholderLocation = -1;
5070
5071 @override
5072 void insertTextPlaceholder(Size size) {
5073 if (!_stylusHandwritingEnabled) {
5074 return;
5075 }
5076
5077 if (!widget.controller.selection.isValid) {
5078 return;
5079 }
5080
5081 setState(() {
5082 _placeholderLocation = _value.text.length - widget.controller.selection.end;
5083 });
5084 }
5085
5086 @override
5087 void removeTextPlaceholder() {
5088 if (!_stylusHandwritingEnabled || _placeholderLocation == -1) {
5089 return;
5090 }
5091
5092 setState(() {
5093 _placeholderLocation = -1;
5094 });
5095 }
5096
5097 @override
5098 void performSelector(String selectorName) {
5099 final Intent? intent = intentForMacOSSelector(selectorName);
5100
5101 if (intent != null) {
5102 final BuildContext? primaryContext = primaryFocus?.context;
5103 if (primaryContext != null) {
5104 Actions.invoke(primaryContext, intent);
5105 }
5106 }
5107 }
5108
5109 @override
5110 String get autofillId => 'EditableText-$hashCode';
5111
5112 int? _viewId;
5113
5114 @override
5115 TextInputConfiguration get textInputConfiguration {
5116 final List<String>? autofillHints = widget.autofillHints?.toList(growable: false);
5117 final AutofillConfiguration autofillConfiguration = autofillHints != null
5118 ? AutofillConfiguration(
5119 uniqueIdentifier: autofillId,
5120 autofillHints: autofillHints,
5121 currentEditingValue: currentTextEditingValue,
5122 )
5123 : AutofillConfiguration.disabled;
5124
5125 _viewId = View.of(context).viewId;
5126 return TextInputConfiguration(
5127 viewId: _viewId,
5128 inputType: widget.keyboardType,
5129 readOnly: widget.readOnly,
5130 obscureText: widget.obscureText,
5131 autocorrect: widget.autocorrect,
5132 smartDashesType: widget.smartDashesType,
5133 smartQuotesType: widget.smartQuotesType,
5134 enableSuggestions: widget.enableSuggestions,
5135 enableInteractiveSelection: widget._userSelectionEnabled,
5136 inputAction:
5137 widget.textInputAction ??
5138 (widget.keyboardType == TextInputType.multiline
5139 ? TextInputAction.newline
5140 : TextInputAction.done),
5141 textCapitalization: widget.textCapitalization,
5142 keyboardAppearance: widget.keyboardAppearance,
5143 autofillConfiguration: autofillConfiguration,
5144 enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,
5145 allowedMimeTypes: widget.contentInsertionConfiguration == null
5146 ? const <String>[]
5147 : widget.contentInsertionConfiguration!.allowedMimeTypes,
5148 hintLocales: widget.hintLocales,
5149 );
5150 }
5151
5152 @override
5153 void autofill(TextEditingValue value) => updateEditingValue(value);
5154
5155 // null if no promptRect should be shown.
5156 TextRange? _currentPromptRectRange;
5157
5158 @override
5159 void showAutocorrectionPromptRect(int start, int end) {
5160 setState(() {
5161 _currentPromptRectRange = TextRange(start: start, end: end);
5162 });
5163 }
5164
5165 VoidCallback? _semanticsOnCopy(TextSelectionControls? controls) {
5166 return widget.selectionEnabled &&
5167 _hasFocus &&
5168 (widget.selectionControls is TextSelectionHandleControls
5169 ? copyEnabled
5170 : copyEnabled && (widget.selectionControls?.canCopy(this) ?? false))
5171 ? () {
5172 controls?.handleCopy(this);
5173 copySelection(SelectionChangedCause.toolbar);
5174 }
5175 : null;
5176 }
5177
5178 VoidCallback? _semanticsOnCut(TextSelectionControls? controls) {
5179 return widget.selectionEnabled &&
5180 _hasFocus &&
5181 (widget.selectionControls is TextSelectionHandleControls
5182 ? cutEnabled
5183 : cutEnabled && (widget.selectionControls?.canCut(this) ?? false))
5184 ? () {
5185 controls?.handleCut(this);
5186 cutSelection(SelectionChangedCause.toolbar);
5187 }
5188 : null;
5189 }
5190
5191 VoidCallback? _semanticsOnPaste(TextSelectionControls? controls) {
5192 return widget.selectionEnabled &&
5193 _hasFocus &&
5194 (widget.selectionControls is TextSelectionHandleControls
5195 ? pasteEnabled
5196 : pasteEnabled && (widget.selectionControls?.canPaste(this) ?? false)) &&
5197 (clipboardStatus.value == ClipboardStatus.pasteable)
5198 ? () {
5199 controls?.handlePaste(this);
5200 pasteText(SelectionChangedCause.toolbar);
5201 }
5202 : null;
5203 }
5204
5205 // Returns the closest boundary location to `extent` but not including `extent`
5206 // itself (unless already at the start/end of the text), in the direction
5207 // specified by `forward`.
5208 TextPosition _moveBeyondTextBoundary(
5209 TextPosition extent,
5210 bool forward,
5211 TextBoundary textBoundary,
5212 ) {
5213 assert(extent.offset >= 0);
5214 final int newOffset = forward
5215 ? textBoundary.getTrailingTextBoundaryAt(extent.offset) ?? _value.text.length
5216 // if x is a boundary defined by `textBoundary`, most textBoundaries (except
5217 // LineBreaker) guarantees `x == textBoundary.getLeadingTextBoundaryAt(x)`.
5218 // Use x - 1 here to make sure we don't get stuck at the fixed point x.
5219 : textBoundary.getLeadingTextBoundaryAt(extent.offset - 1) ?? 0;
5220 return TextPosition(offset: newOffset);
5221 }
5222
5223 // Returns the closest boundary location to `extent`, including `extent`
5224 // itself, in the direction specified by `forward`.
5225 //
5226 // This method returns a fixed point of itself: applying `_toTextBoundary`
5227 // again on the returned TextPosition gives the same TextPosition. It's used
5228 // exclusively for handling line boundaries, since performing "move to line
5229 // start" more than once usually doesn't move you to the previous line.
5230 TextPosition _moveToTextBoundary(TextPosition extent, bool forward, TextBoundary textBoundary) {
5231 assert(extent.offset >= 0);
5232 final int caretOffset;
5233 switch (extent.affinity) {
5234 case TextAffinity.upstream:
5235 if (extent.offset < 1 && !forward) {
5236 assert(extent.offset == 0);
5237 return const TextPosition(offset: 0);
5238 }
5239 // When the text affinity is upstream, the caret is associated with the
5240 // grapheme before the code unit at `extent.offset`.
5241 // TODO(LongCatIsLooong): don't assume extent.offset is at a grapheme
5242 // boundary, and do this instead:
5243 // final int graphemeStart = CharacterRange.at(string, extent.offset).stringBeforeLength - 1;
5244 caretOffset = math.max(0, extent.offset - 1);
5245 case TextAffinity.downstream:
5246 caretOffset = extent.offset;
5247 }
5248 // The line boundary range does not include some control characters
5249 // (most notably, Line Feed), in which case there's
5250 // `x ∉ getTextBoundaryAt(x)`. In case `caretOffset` points to one such
5251 // control character, we define that these control characters themselves are
5252 // still part of the previous line, but also exclude them from the
5253 // line boundary range since they're non-printing. IOW, no additional
5254 // processing needed since the LineBoundary class does exactly that.
5255 return forward
5256 ? TextPosition(
5257 offset: textBoundary.getTrailingTextBoundaryAt(caretOffset) ?? _value.text.length,
5258 affinity: TextAffinity.upstream,
5259 )
5260 : TextPosition(offset: textBoundary.getLeadingTextBoundaryAt(caretOffset) ?? 0);
5261 }
5262
5263 // --------------------------- Text Editing Actions ---------------------------
5264
5265 TextBoundary _characterBoundary() =>
5266 widget.obscureText ? _CodePointBoundary(_value.text) : CharacterBoundary(_value.text);
5267 TextBoundary _nextWordBoundary() =>
5268 widget.obscureText ? _documentBoundary() : renderEditable.wordBoundaries.moveByWordBoundary;
5269 TextBoundary _linebreak() =>
5270 widget.obscureText ? _documentBoundary() : LineBoundary(renderEditable);
5271 TextBoundary _paragraphBoundary() => ParagraphBoundary(_value.text);
5272 TextBoundary _documentBoundary() => DocumentBoundary(_value.text);
5273
5274 Action<T> _makeOverridable<T extends Intent>(Action<T> defaultAction) {
5275 return Action<T>.overridable(context: context, defaultAction: defaultAction);
5276 }
5277
5278 /// Transpose the characters immediately before and after the current
5279 /// collapsed selection.
5280 ///
5281 /// When the cursor is at the end of the text, transposes the last two
5282 /// characters, if they exist.
5283 ///
5284 /// When the cursor is at the start of the text, does nothing.
5285 void _transposeCharacters(TransposeCharactersIntent intent) {
5286 if (_value.text.characters.length <= 1 ||
5287 !_value.selection.isCollapsed ||
5288 _value.selection.baseOffset == 0) {
5289 return;
5290 }
5291
5292 final String text = _value.text;
5293 final TextSelection selection = _value.selection;
5294 final bool atEnd = selection.baseOffset == text.length;
5295 final CharacterRange transposing = CharacterRange.at(text, selection.baseOffset);
5296 if (atEnd) {
5297 transposing.moveBack(2);
5298 } else {
5299 transposing
5300 ..moveBack()
5301 ..expandNext();
5302 }
5303 assert(transposing.currentCharacters.length == 2);
5304
5305 userUpdateTextEditingValue(
5306 TextEditingValue(
5307 text:
5308 transposing.stringBefore +
5309 transposing.currentCharacters.last +
5310 transposing.currentCharacters.first +
5311 transposing.stringAfter,
5312 selection: TextSelection.collapsed(
5313 offset: transposing.stringBeforeLength + transposing.current.length,
5314 ),
5315 ),
5316 SelectionChangedCause.keyboard,
5317 );
5318 }
5319
5320 late final Action<TransposeCharactersIntent> _transposeCharactersAction =
5321 CallbackAction<TransposeCharactersIntent>(onInvoke: _transposeCharacters);
5322
5323 void _replaceText(ReplaceTextIntent intent) {
5324 final TextEditingValue oldValue = _value;
5325 final TextEditingValue newValue = intent.currentTextEditingValue.replaced(
5326 intent.replacementRange,
5327 intent.replacementText,
5328 );
5329 userUpdateTextEditingValue(newValue, intent.cause);
5330
5331 // If there's no change in text and selection (e.g. when selecting and
5332 // pasting identical text), the widget won't be rebuilt on value update.
5333 // Handle this by calling _didChangeTextEditingValue() so caret and scroll
5334 // updates can happen.
5335 if (newValue == oldValue) {
5336 _didChangeTextEditingValue();
5337 }
5338 }
5339
5340 late final Action<ReplaceTextIntent> _replaceTextAction = CallbackAction<ReplaceTextIntent>(
5341 onInvoke: _replaceText,
5342 );
5343
5344 // Scrolls either to the beginning or end of the document depending on the
5345 // intent's `forward` parameter.
5346 void _scrollToDocumentBoundary(ScrollToDocumentBoundaryIntent intent) {
5347 if (intent.forward) {
5348 bringIntoView(TextPosition(offset: _value.text.length));
5349 } else {
5350 bringIntoView(const TextPosition(offset: 0));
5351 }
5352 }
5353
5354 /// Handles [ScrollIntent] by scrolling the [Scrollable] inside of
5355 /// [EditableText].
5356 void _scroll(ScrollIntent intent) {
5357 if (intent.type != ScrollIncrementType.page) {
5358 return;
5359 }
5360
5361 final ScrollPosition position = _scrollController.position;
5362 if (widget.maxLines == 1) {
5363 _scrollController.jumpTo(position.maxScrollExtent);
5364 return;
5365 }
5366
5367 // If the field isn't scrollable, do nothing. For example, when the lines of
5368 // text is less than maxLines, the field has nothing to scroll.
5369 if (position.maxScrollExtent == 0.0 && position.minScrollExtent == 0.0) {
5370 return;
5371 }
5372
5373 final ScrollableState? state = _scrollableKey.currentState as ScrollableState?;
5374 final double increment = ScrollAction.getDirectionalIncrement(state!, intent);
5375 final double destination = clampDouble(
5376 position.pixels + increment,
5377 position.minScrollExtent,
5378 position.maxScrollExtent,
5379 );
5380 if (destination == position.pixels) {
5381 return;
5382 }
5383 _scrollController.jumpTo(destination);
5384 }
5385
5386 /// Extend the selection down by page if the `forward` parameter is true, or
5387 /// up by page otherwise.
5388 void _extendSelectionByPage(ExtendSelectionByPageIntent intent) {
5389 if (widget.maxLines == 1) {
5390 return;
5391 }
5392
5393 final TextSelection nextSelection;
5394 final Rect extentRect = renderEditable.getLocalRectForCaret(_value.selection.extent);
5395 final ScrollableState? state = _scrollableKey.currentState as ScrollableState?;
5396 final double increment = ScrollAction.getDirectionalIncrement(
5397 state!,
5398 ScrollIntent(
5399 direction: intent.forward ? AxisDirection.down : AxisDirection.up,
5400 type: ScrollIncrementType.page,
5401 ),
5402 );
5403 final ScrollPosition position = _scrollController.position;
5404 if (intent.forward) {
5405 if (_value.selection.extentOffset >= _value.text.length) {
5406 return;
5407 }
5408 final Offset nextExtentOffset = Offset(extentRect.left, extentRect.top + increment);
5409 final double height = position.maxScrollExtent + renderEditable.size.height;
5410 final TextPosition nextExtent = nextExtentOffset.dy + position.pixels >= height
5411 ? TextPosition(offset: _value.text.length)
5412 : renderEditable.getPositionForPoint(renderEditable.localToGlobal(nextExtentOffset));
5413 nextSelection = _value.selection.copyWith(extentOffset: nextExtent.offset);
5414 } else {
5415 if (_value.selection.extentOffset <= 0) {
5416 return;
5417 }
5418 final Offset nextExtentOffset = Offset(extentRect.left, extentRect.top + increment);
5419 final TextPosition nextExtent = nextExtentOffset.dy + position.pixels <= 0
5420 ? const TextPosition(offset: 0)
5421 : renderEditable.getPositionForPoint(renderEditable.localToGlobal(nextExtentOffset));
5422 nextSelection = _value.selection.copyWith(extentOffset: nextExtent.offset);
5423 }
5424
5425 bringIntoView(nextSelection.extent);
5426 userUpdateTextEditingValue(
5427 _value.copyWith(selection: nextSelection),
5428 SelectionChangedCause.keyboard,
5429 );
5430 }
5431
5432 void _updateSelection(UpdateSelectionIntent intent) {
5433 assert(
5434 intent.newSelection.start <= intent.currentTextEditingValue.text.length,
5435 'invalid selection: ${intent.newSelection}: it must not exceed the current text length ${intent.currentTextEditingValue.text.length}',
5436 );
5437 assert(
5438 intent.newSelection.end <= intent.currentTextEditingValue.text.length,
5439 'invalid selection: ${intent.newSelection}: it must not exceed the current text length ${intent.currentTextEditingValue.text.length}',
5440 );
5441
5442 bringIntoView(intent.newSelection.extent);
5443 userUpdateTextEditingValue(
5444 intent.currentTextEditingValue.copyWith(selection: intent.newSelection),
5445 intent.cause,
5446 );
5447 }
5448
5449 late final Action<UpdateSelectionIntent> _updateSelectionAction =
5450 CallbackAction<UpdateSelectionIntent>(onInvoke: _updateSelection);
5451
5452 late final _UpdateTextSelectionVerticallyAction<DirectionalCaretMovementIntent>
5453 _verticalSelectionUpdateAction =
5454 _UpdateTextSelectionVerticallyAction<DirectionalCaretMovementIntent>(this);
5455
5456 Object? _hideToolbarIfVisible(DismissIntent intent) {
5457 if (_selectionOverlay?.toolbarIsVisible ?? false) {
5458 hideToolbar(false);
5459 return null;
5460 }
5461 return Actions.invoke(context, intent);
5462 }
5463
5464 void _onTapOutside(BuildContext context, PointerDownEvent event) {
5465 _hadFocusOnTapDown = true;
5466
5467 if (widget.onTapOutside != null) {
5468 widget.onTapOutside!(event);
5469 } else {
5470 _defaultOnTapOutside(context, event);
5471 }
5472 }
5473
5474 void _onTapUpOutside(BuildContext context, PointerUpEvent event) {
5475 if (!_hadFocusOnTapDown) {
5476 return;
5477 }
5478
5479 // Reset to false so that subsequent events doesn't trigger the callback based on old information.
5480 _hadFocusOnTapDown = false;
5481
5482 if (widget.onTapUpOutside != null) {
5483 widget.onTapUpOutside!(event);
5484 } else {
5485 _defaultOnTapUpOutside(context, event);
5486 }
5487 }
5488
5489 /// The default behavior used if [EditableText.onTapOutside] is null.
5490 ///
5491 /// The `event` argument is the [PointerDownEvent] that caused the notification.
5492 void _defaultOnTapOutside(BuildContext context, PointerDownEvent event) {
5493 Actions.invoke(
5494 context,
5495 EditableTextTapOutsideIntent(focusNode: widget.focusNode, pointerDownEvent: event),
5496 );
5497 }
5498
5499 /// The default behavior used if [EditableText.onTapUpOutside] is null.
5500 ///
5501 /// The `event` argument is the [PointerUpEvent] that caused the notification.
5502 void _defaultOnTapUpOutside(BuildContext context, PointerUpEvent event) {
5503 Actions.invoke(
5504 context,
5505 EditableTextTapUpOutsideIntent(focusNode: widget.focusNode, pointerUpEvent: event),
5506 );
5507 }
5508
5509 late final Map<Type, Action<Intent>> _actions = <Type, Action<Intent>>{
5510 DoNothingAndStopPropagationTextIntent: DoNothingAction(consumesKey: false),
5511 ReplaceTextIntent: _replaceTextAction,
5512 UpdateSelectionIntent: _updateSelectionAction,
5513 DirectionalFocusIntent: DirectionalFocusAction.forTextField(),
5514 DismissIntent: CallbackAction<DismissIntent>(onInvoke: _hideToolbarIfVisible),
5515
5516 // Delete
5517 DeleteCharacterIntent: _makeOverridable(
5518 _DeleteTextAction<DeleteCharacterIntent>(this, _characterBoundary, _moveBeyondTextBoundary),
5519 ),
5520 DeleteToNextWordBoundaryIntent: _makeOverridable(
5521 _DeleteTextAction<DeleteToNextWordBoundaryIntent>(
5522 this,
5523 _nextWordBoundary,
5524 _moveBeyondTextBoundary,
5525 ),
5526 ),
5527 DeleteToLineBreakIntent: _makeOverridable(
5528 _DeleteTextAction<DeleteToLineBreakIntent>(this, _linebreak, _moveToTextBoundary),
5529 ),
5530
5531 // Extend/Move Selection
5532 ExtendSelectionByCharacterIntent: _makeOverridable(
5533 _UpdateTextSelectionAction<ExtendSelectionByCharacterIntent>(
5534 this,
5535 _characterBoundary,
5536 _moveBeyondTextBoundary,
5537 ignoreNonCollapsedSelection: false,
5538 ),
5539 ),
5540 ExtendSelectionByPageIntent: _makeOverridable(
5541 CallbackAction<ExtendSelectionByPageIntent>(onInvoke: _extendSelectionByPage),
5542 ),
5543 ExtendSelectionToNextWordBoundaryIntent: _makeOverridable(
5544 _UpdateTextSelectionAction<ExtendSelectionToNextWordBoundaryIntent>(
5545 this,
5546 _nextWordBoundary,
5547 _moveBeyondTextBoundary,
5548 ignoreNonCollapsedSelection: true,
5549 ),
5550 ),
5551 ExtendSelectionToNextParagraphBoundaryIntent: _makeOverridable(
5552 _UpdateTextSelectionAction<ExtendSelectionToNextParagraphBoundaryIntent>(
5553 this,
5554 _paragraphBoundary,
5555 _moveBeyondTextBoundary,
5556 ignoreNonCollapsedSelection: true,
5557 ),
5558 ),
5559 ExtendSelectionToLineBreakIntent: _makeOverridable(
5560 _UpdateTextSelectionAction<ExtendSelectionToLineBreakIntent>(
5561 this,
5562 _linebreak,
5563 _moveToTextBoundary,
5564 ignoreNonCollapsedSelection: true,
5565 ),
5566 ),
5567 ExtendSelectionVerticallyToAdjacentLineIntent: _makeOverridable(_verticalSelectionUpdateAction),
5568 ExtendSelectionVerticallyToAdjacentPageIntent: _makeOverridable(_verticalSelectionUpdateAction),
5569 ExtendSelectionToNextParagraphBoundaryOrCaretLocationIntent: _makeOverridable(
5570 _UpdateTextSelectionAction<ExtendSelectionToNextParagraphBoundaryOrCaretLocationIntent>(
5571 this,
5572 _paragraphBoundary,
5573 _moveBeyondTextBoundary,
5574 ignoreNonCollapsedSelection: true,
5575 ),
5576 ),
5577 ExtendSelectionToDocumentBoundaryIntent: _makeOverridable(
5578 _UpdateTextSelectionAction<ExtendSelectionToDocumentBoundaryIntent>(
5579 this,
5580 _documentBoundary,
5581 _moveBeyondTextBoundary,
5582 ignoreNonCollapsedSelection: true,
5583 ),
5584 ),
5585 ExtendSelectionToNextWordBoundaryOrCaretLocationIntent: _makeOverridable(
5586 _UpdateTextSelectionAction<ExtendSelectionToNextWordBoundaryOrCaretLocationIntent>(
5587 this,
5588 _nextWordBoundary,
5589 _moveBeyondTextBoundary,
5590 ignoreNonCollapsedSelection: true,
5591 ),
5592 ),
5593 ScrollToDocumentBoundaryIntent: _makeOverridable(
5594 _WebComposingDisablingCallbackAction<ScrollToDocumentBoundaryIntent>(
5595 this,
5596 onInvoke: _scrollToDocumentBoundary,
5597 ),
5598 ),
5599 ScrollIntent: CallbackAction<ScrollIntent>(onInvoke: _scroll),
5600
5601 // Expand Selection
5602 ExpandSelectionToLineBreakIntent: _makeOverridable(
5603 _UpdateTextSelectionAction<ExpandSelectionToLineBreakIntent>(
5604 this,
5605 _linebreak,
5606 _moveToTextBoundary,
5607 ignoreNonCollapsedSelection: true,
5608 isExpand: true,
5609 ),
5610 ),
5611 ExpandSelectionToDocumentBoundaryIntent: _makeOverridable(
5612 _UpdateTextSelectionAction<ExpandSelectionToDocumentBoundaryIntent>(
5613 this,
5614 _documentBoundary,
5615 _moveToTextBoundary,
5616 ignoreNonCollapsedSelection: true,
5617 isExpand: true,
5618 extentAtIndex: true,
5619 ),
5620 ),
5621
5622 // Copy Paste
5623 SelectAllTextIntent: _makeOverridable(_SelectAllAction(this)),
5624 CopySelectionTextIntent: _makeOverridable(_CopySelectionAction(this)),
5625 PasteTextIntent: _makeOverridable(_PasteSelectionAction(this)),
5626
5627 TransposeCharactersIntent: _makeOverridable(_transposeCharactersAction),
5628 EditableTextTapOutsideIntent: _makeOverridable(_EditableTextTapOutsideAction()),
5629 EditableTextTapUpOutsideIntent: _makeOverridable(_EditableTextTapUpOutsideAction()),
5630 };
5631
5632 @protected
5633 @override
5634 Widget build(BuildContext context) {
5635 assert(debugCheckHasMediaQuery(context));
5636 super.build(context); // See AutomaticKeepAliveClientMixin.
5637
5638 final TextSelectionControls? controls = widget.selectionControls;
5639 final TextScaler effectiveTextScaler = switch ((widget.textScaler, widget.textScaleFactor)) {
5640 (final TextScaler textScaler, _) => textScaler,
5641 (null, final double textScaleFactor) => TextScaler.linear(textScaleFactor),
5642 (null, null) => MediaQuery.textScalerOf(context),
5643 };
5644 final ui.SemanticsInputType inputType;
5645 switch (widget.keyboardType) {
5646 case TextInputType.phone:
5647 inputType = ui.SemanticsInputType.phone;
5648 case TextInputType.url:
5649 inputType = ui.SemanticsInputType.url;
5650 case TextInputType.emailAddress:
5651 inputType = ui.SemanticsInputType.email;
5652 default:
5653 inputType = ui.SemanticsInputType.text;
5654 }
5655
5656 return _CompositionCallback(
5657 compositeCallback: _compositeCallback,
5658 enabled: _hasInputConnection,
5659 child: Actions(
5660 actions: _actions,
5661 child: Builder(
5662 builder: (BuildContext context) {
5663 return TextFieldTapRegion(
5664 groupId: widget.groupId,
5665 onTapOutside: _hasFocus
5666 ? (PointerDownEvent event) => _onTapOutside(context, event)
5667 : null,
5668 onTapUpOutside: (PointerUpEvent event) => _onTapUpOutside(context, event),
5669 debugLabel: kReleaseMode ? null : 'EditableText',
5670 child: MouseRegion(
5671 cursor: widget.mouseCursor ?? SystemMouseCursors.text,
5672 child: UndoHistory<TextEditingValue>(
5673 value: widget.controller,
5674 onTriggered: (TextEditingValue value) {
5675 userUpdateTextEditingValue(value, SelectionChangedCause.keyboard);
5676 },
5677 shouldChangeUndoStack: (TextEditingValue? oldValue, TextEditingValue newValue) {
5678 if (!newValue.selection.isValid) {
5679 return false;
5680 }
5681
5682 if (oldValue == null) {
5683 return true;
5684 }
5685
5686 switch (defaultTargetPlatform) {
5687 case TargetPlatform.iOS:
5688 case TargetPlatform.macOS:
5689 case TargetPlatform.fuchsia:
5690 case TargetPlatform.linux:
5691 case TargetPlatform.windows:
5692 // Composing text is not counted in history coalescing.
5693 if (!widget.controller.value.composing.isCollapsed) {
5694 return false;
5695 }
5696 case TargetPlatform.android:
5697 // Gboard on Android puts non-CJK words in composing regions. Coalesce
5698 // composing text in order to allow the saving of partial words in that
5699 // case.
5700 break;
5701 }
5702
5703 return oldValue.text != newValue.text ||
5704 oldValue.composing != newValue.composing;
5705 },
5706 undoStackModifier: (TextEditingValue value) {
5707 // On Android we should discard the composing region when pushing
5708 // a new entry to the undo stack. This prevents the TextInputPlugin
5709 // from restarting the input on every undo/redo when the composing
5710 // region is changed by the framework.
5711 return defaultTargetPlatform == TargetPlatform.android
5712 ? value.copyWith(composing: TextRange.empty)
5713 : value;
5714 },
5715 focusNode: widget.focusNode,
5716 controller: widget.undoController,
5717 child: Focus(
5718 focusNode: widget.focusNode,
5719 includeSemantics: false,
5720 debugLabel: kReleaseMode ? null : 'EditableText',
5721 child: NotificationListener<ScrollNotification>(
5722 onNotification: (ScrollNotification notification) {
5723 _handleContextMenuOnScroll(notification);
5724 _scribbleCacheKey = null;
5725 return false;
5726 },
5727 child: Scrollable(
5728 key: _scrollableKey,
5729 excludeFromSemantics: true,
5730 axisDirection: _isMultiline ? AxisDirection.down : AxisDirection.right,
5731 controller: _scrollController,
5732 // On iOS a single-line TextField should not scroll.
5733 physics:
5734 widget.scrollPhysics ??
5735 (!_isMultiline && defaultTargetPlatform == TargetPlatform.iOS
5736 ? const _NeverUserScrollableScrollPhysics()
5737 : null),
5738 dragStartBehavior: widget.dragStartBehavior,
5739 restorationId: widget.restorationId,
5740 // If a ScrollBehavior is not provided, only apply scrollbars when
5741 // multiline. The overscroll indicator should not be applied in
5742 // either case, glowing or stretching.
5743 scrollBehavior:
5744 widget.scrollBehavior ??
5745 ScrollConfiguration.of(
5746 context,
5747 ).copyWith(scrollbars: _isMultiline, overscroll: false),
5748 viewportBuilder: (BuildContext context, ViewportOffset offset) {
5749 return CompositedTransformTarget(
5750 link: _toolbarLayerLink,
5751 child: Semantics(
5752 inputType: inputType,
5753 onCopy: _semanticsOnCopy(controls),
5754 onCut: _semanticsOnCut(controls),
5755 onPaste: _semanticsOnPaste(controls),
5756 child: _ScribbleFocusable(
5757 editableKey: _editableKey,
5758 enabled: _stylusHandwritingEnabled,
5759 focusNode: widget.focusNode,
5760 updateSelectionRects: () {
5761 _openInputConnection();
5762 _updateSelectionRects(force: true);
5763 },
5764 child: SizeChangedLayoutNotifier(
5765 child: _Editable(
5766 key: _editableKey,
5767 startHandleLayerLink: _startHandleLayerLink,
5768 endHandleLayerLink: _endHandleLayerLink,
5769 inlineSpan: buildTextSpan(),
5770 value: _value,
5771 cursorColor: _cursorColor,
5772 backgroundCursorColor: widget.backgroundCursorColor,
5773 showCursor: _cursorVisibilityNotifier,
5774 forceLine: widget.forceLine,
5775 readOnly: widget.readOnly,
5776 hasFocus: _hasFocus,
5777 maxLines: widget.maxLines,
5778 minLines: widget.minLines,
5779 expands: widget.expands,
5780 strutStyle: widget.strutStyle,
5781 selectionColor:
5782 _selectionOverlay?.spellCheckToolbarIsVisible ?? false
5783 ? _spellCheckConfiguration.misspelledSelectionColor ??
5784 widget.selectionColor
5785 : widget.selectionColor,
5786 textScaler: effectiveTextScaler,
5787 textAlign: widget.textAlign,
5788 textDirection: _textDirection,
5789 locale: widget.locale,
5790 textHeightBehavior:
5791 widget.textHeightBehavior ??
5792 DefaultTextHeightBehavior.maybeOf(context),
5793 textWidthBasis: widget.textWidthBasis,
5794 obscuringCharacter: widget.obscuringCharacter,
5795 obscureText: widget.obscureText,
5796 offset: offset,
5797 rendererIgnoresPointer: widget.rendererIgnoresPointer,
5798 cursorWidth: widget.cursorWidth,
5799 cursorHeight: widget.cursorHeight,
5800 cursorRadius: widget.cursorRadius,
5801 cursorOffset: widget.cursorOffset ?? Offset.zero,
5802 selectionHeightStyle: widget.selectionHeightStyle,
5803 selectionWidthStyle: widget.selectionWidthStyle,
5804 paintCursorAboveText: widget.paintCursorAboveText,
5805 enableInteractiveSelection: widget._userSelectionEnabled,
5806 textSelectionDelegate: this,
5807 devicePixelRatio: _devicePixelRatio,
5808 promptRectRange: _currentPromptRectRange,
5809 promptRectColor: widget.autocorrectionTextRectColor,
5810 clipBehavior: widget.clipBehavior,
5811 ),
5812 ),
5813 ),
5814 ),
5815 );
5816 },
5817 ),
5818 ),
5819 ),
5820 ),
5821 ),
5822 );
5823 },
5824 ),
5825 ),
5826 );
5827 }
5828
5829 /// Builds [TextSpan] from current editing value.
5830 ///
5831 /// By default makes text in composing range appear as underlined.
5832 /// Descendants can override this method to customize appearance of text.
5833 TextSpan buildTextSpan() {
5834 if (widget.obscureText) {
5835 String text = _value.text;
5836 text = widget.obscuringCharacter * text.length;
5837 // Reveal the latest character in an obscured field only on mobile.
5838 const Set<TargetPlatform> mobilePlatforms = <TargetPlatform>{
5839 TargetPlatform.android,
5840 TargetPlatform.fuchsia,
5841 TargetPlatform.iOS,
5842 };
5843 final bool brieflyShowPassword =
5844 WidgetsBinding.instance.platformDispatcher.brieflyShowPassword &&
5845 mobilePlatforms.contains(defaultTargetPlatform);
5846 if (brieflyShowPassword) {
5847 final int? o = _obscureShowCharTicksPending > 0 ? _obscureLatestCharIndex : null;
5848 if (o != null && o >= 0 && o < text.length) {
5849 text = text.replaceRange(o, o + 1, _value.text.substring(o, o + 1));
5850 }
5851 }
5852 return TextSpan(style: _style, text: text);
5853 }
5854 if (_placeholderLocation >= 0 && _placeholderLocation <= _value.text.length) {
5855 final List<_ScribblePlaceholder> placeholders = <_ScribblePlaceholder>[];
5856 final int placeholderLocation = _value.text.length - _placeholderLocation;
5857 if (_isMultiline) {
5858 // The zero size placeholder here allows the line to break and keep the caret on the first line.
5859 placeholders.add(const _ScribblePlaceholder(child: SizedBox.shrink(), size: Size.zero));
5860 placeholders.add(
5861 _ScribblePlaceholder(
5862 child: const SizedBox.shrink(),
5863 size: Size(renderEditable.size.width, 0.0),
5864 ),
5865 );
5866 } else {
5867 placeholders.add(
5868 const _ScribblePlaceholder(child: SizedBox.shrink(), size: Size(100.0, 0.0)),
5869 );
5870 }
5871 return TextSpan(
5872 style: _style,
5873 children: <InlineSpan>[
5874 TextSpan(text: _value.text.substring(0, placeholderLocation)),
5875 ...placeholders,
5876 TextSpan(text: _value.text.substring(placeholderLocation)),
5877 ],
5878 );
5879 }
5880 final bool withComposing = !widget.readOnly && _hasFocus;
5881 if (_spellCheckResultsReceived) {
5882 // If the composing range is out of range for the current text, ignore it to
5883 // preserve the tree integrity, otherwise in release mode a RangeError will
5884 // be thrown and this EditableText will be built with a broken subtree.
5885 assert(!_value.composing.isValid || !withComposing || _value.isComposingRangeValid);
5886
5887 final bool composingRegionOutOfRange = !_value.isComposingRangeValid || !withComposing;
5888
5889 return buildTextSpanWithSpellCheckSuggestions(
5890 _value,
5891 composingRegionOutOfRange,
5892 _style,
5893 _spellCheckConfiguration.misspelledTextStyle!,
5894 spellCheckResults!,
5895 );
5896 }
5897
5898 // Read only mode should not paint text composing.
5899 return widget.controller.buildTextSpan(
5900 context: context,
5901 style: _style,
5902 withComposing: withComposing,
5903 );
5904 }
5905 }
5906
5907 class _Editable extends MultiChildRenderObjectWidget {
5908 _Editable({
5909 super.key,
5910 required this.inlineSpan,
5911 required this.value,
5912 required this.startHandleLayerLink,
5913 required this.endHandleLayerLink,
5914 this.cursorColor,
5915 this.backgroundCursorColor,
5916 required this.showCursor,
5917 required this.forceLine,
5918 required this.readOnly,
5919 this.textHeightBehavior,
5920 required this.textWidthBasis,
5921 required this.hasFocus,
5922 required this.maxLines,
5923 this.minLines,
5924 required this.expands,
5925 this.strutStyle,
5926 this.selectionColor,
5927 required this.textScaler,
5928 required this.textAlign,
5929 required this.textDirection,
5930 this.locale,
5931 required this.obscuringCharacter,
5932 required this.obscureText,
5933 required this.offset,
5934 this.rendererIgnoresPointer = false,
5935 required this.cursorWidth,
5936 this.cursorHeight,
5937 this.cursorRadius,
5938 required this.cursorOffset,
5939 required this.paintCursorAboveText,
5940 ui.BoxHeightStyle? selectionHeightStyle,
5941 ui.BoxWidthStyle? selectionWidthStyle,
5942 this.enableInteractiveSelection = true,
5943 required this.textSelectionDelegate,
5944 required this.devicePixelRatio,
5945 this.promptRectRange,
5946 this.promptRectColor,
5947 required this.clipBehavior,
5948 }) : selectionHeightStyle = selectionHeightStyle ?? EditableText.defaultSelectionHeightStyle,
5949 selectionWidthStyle = selectionWidthStyle ?? EditableText.defaultSelectionWidthStyle,
5950 super(children: WidgetSpan.extractFromInlineSpan(inlineSpan, textScaler));
5951
5952 final InlineSpan inlineSpan;
5953 final TextEditingValue value;
5954 final Color? cursorColor;
5955 final LayerLink startHandleLayerLink;
5956 final LayerLink endHandleLayerLink;
5957 final Color? backgroundCursorColor;
5958 final ValueNotifier<bool> showCursor;
5959 final bool forceLine;
5960 final bool readOnly;
5961 final bool hasFocus;
5962 final int? maxLines;
5963 final int? minLines;
5964 final bool expands;
5965 final StrutStyle? strutStyle;
5966 final Color? selectionColor;
5967 final TextScaler textScaler;
5968 final TextAlign textAlign;
5969 final TextDirection textDirection;
5970 final Locale? locale;
5971 final String obscuringCharacter;
5972 final bool obscureText;
5973 final TextHeightBehavior? textHeightBehavior;
5974 final TextWidthBasis textWidthBasis;
5975 final ViewportOffset offset;
5976 final bool rendererIgnoresPointer;
5977 final double cursorWidth;
5978 final double? cursorHeight;
5979 final Radius? cursorRadius;
5980 final Offset cursorOffset;
5981 final bool paintCursorAboveText;
5982 final ui.BoxHeightStyle selectionHeightStyle;
5983 final ui.BoxWidthStyle selectionWidthStyle;
5984 final bool enableInteractiveSelection;
5985 final TextSelectionDelegate textSelectionDelegate;
5986 final double devicePixelRatio;
5987 final TextRange? promptRectRange;
5988 final Color? promptRectColor;
5989 final Clip clipBehavior;
5990
5991 @override
5992 RenderEditable createRenderObject(BuildContext context) {
5993 return RenderEditable(
5994 text: inlineSpan,
5995 cursorColor: cursorColor,
5996 startHandleLayerLink: startHandleLayerLink,
5997 endHandleLayerLink: endHandleLayerLink,
5998 backgroundCursorColor: backgroundCursorColor,
5999 showCursor: showCursor,
6000 forceLine: forceLine,
6001 readOnly: readOnly,
6002 hasFocus: hasFocus,
6003 maxLines: maxLines,
6004 minLines: minLines,
6005 expands: expands,
6006 strutStyle: strutStyle,
6007 selectionColor: selectionColor,
6008 textScaler: textScaler,
6009 textAlign: textAlign,
6010 textDirection: textDirection,
6011 locale: locale ?? Localizations.maybeLocaleOf(context),
6012 selection: value.selection,
6013 offset: offset,
6014 ignorePointer: rendererIgnoresPointer,
6015 obscuringCharacter: obscuringCharacter,
6016 obscureText: obscureText,
6017 textHeightBehavior: textHeightBehavior,
6018 textWidthBasis: textWidthBasis,
6019 cursorWidth: cursorWidth,
6020 cursorHeight: cursorHeight,
6021 cursorRadius: cursorRadius,
6022 cursorOffset: cursorOffset,
6023 paintCursorAboveText: paintCursorAboveText,
6024 selectionHeightStyle: selectionHeightStyle,
6025 selectionWidthStyle: selectionWidthStyle,
6026 enableInteractiveSelection: enableInteractiveSelection,
6027 textSelectionDelegate: textSelectionDelegate,
6028 devicePixelRatio: devicePixelRatio,
6029 promptRectRange: promptRectRange,
6030 promptRectColor: promptRectColor,
6031 clipBehavior: clipBehavior,
6032 );
6033 }
6034
6035 @override
6036 void updateRenderObject(BuildContext context, RenderEditable renderObject) {
6037 renderObject
6038 ..text = inlineSpan
6039 ..cursorColor = cursorColor
6040 ..startHandleLayerLink = startHandleLayerLink
6041 ..endHandleLayerLink = endHandleLayerLink
6042 ..backgroundCursorColor = backgroundCursorColor
6043 ..showCursor = showCursor
6044 ..forceLine = forceLine
6045 ..readOnly = readOnly
6046 ..hasFocus = hasFocus
6047 ..maxLines = maxLines
6048 ..minLines = minLines
6049 ..expands = expands
6050 ..strutStyle = strutStyle
6051 ..selectionColor = selectionColor
6052 ..textScaler = textScaler
6053 ..textAlign = textAlign
6054 ..textDirection = textDirection
6055 ..locale = locale ?? Localizations.maybeLocaleOf(context)
6056 ..selection = value.selection
6057 ..offset = offset
6058 ..ignorePointer = rendererIgnoresPointer
6059 ..textHeightBehavior = textHeightBehavior
6060 ..textWidthBasis = textWidthBasis
6061 ..obscuringCharacter = obscuringCharacter
6062 ..obscureText = obscureText
6063 ..cursorWidth = cursorWidth
6064 ..cursorHeight = cursorHeight
6065 ..cursorRadius = cursorRadius
6066 ..cursorOffset = cursorOffset
6067 ..selectionHeightStyle = selectionHeightStyle
6068 ..selectionWidthStyle = selectionWidthStyle
6069 ..enableInteractiveSelection = enableInteractiveSelection
6070 ..textSelectionDelegate = textSelectionDelegate
6071 ..devicePixelRatio = devicePixelRatio
6072 ..paintCursorAboveText = paintCursorAboveText
6073 ..promptRectColor = promptRectColor
6074 ..clipBehavior = clipBehavior
6075 ..setPromptRectRange(promptRectRange);
6076 }
6077 }
6078
6079 class _NeverUserScrollableScrollPhysics extends ScrollPhysics {
6080 /// Creates a scroll physics that prevents scrolling with user input, for example
6081 /// by dragging, but still allows for programmatic scrolling.
6082 const _NeverUserScrollableScrollPhysics({super.parent});
6083
6084 @override
6085 _NeverUserScrollableScrollPhysics applyTo(ScrollPhysics? ancestor) {
6086 return _NeverUserScrollableScrollPhysics(parent: buildParent(ancestor));
6087 }
6088
6089 @override
6090 bool get allowUserScrolling => false;
6091 }
6092
6093 @immutable
6094 class _ScribbleCacheKey {
6095 const _ScribbleCacheKey({
6096 required this.inlineSpan,
6097 required this.textAlign,
6098 required this.textDirection,
6099 required this.textScaler,
6100 required this.textHeightBehavior,
6101 required this.locale,
6102 required this.structStyle,
6103 required this.placeholder,
6104 required this.size,
6105 });
6106
6107 final TextAlign textAlign;
6108 final TextDirection textDirection;
6109 final TextScaler textScaler;
6110 final TextHeightBehavior? textHeightBehavior;
6111 final Locale? locale;
6112 final StrutStyle structStyle;
6113 final int placeholder;
6114 final Size size;
6115 final InlineSpan inlineSpan;
6116
6117 RenderComparison compare(_ScribbleCacheKey other) {
6118 if (identical(other, this)) {
6119 return RenderComparison.identical;
6120 }
6121 final bool needsLayout =
6122 textAlign != other.textAlign ||
6123 textDirection != other.textDirection ||
6124 textScaler != other.textScaler ||
6125 (textHeightBehavior ?? const TextHeightBehavior()) !=
6126 (other.textHeightBehavior ?? const TextHeightBehavior()) ||
6127 locale != other.locale ||
6128 structStyle != other.structStyle ||
6129 placeholder != other.placeholder ||
6130 size != other.size;
6131 return needsLayout ? RenderComparison.layout : inlineSpan.compareTo(other.inlineSpan);
6132 }
6133 }
6134
6135 class _ScribbleFocusable extends StatefulWidget {
6136 const _ScribbleFocusable({
6137 required this.child,
6138 required this.focusNode,
6139 required this.editableKey,
6140 required this.updateSelectionRects,
6141 required this.enabled,
6142 });
6143
6144 final Widget child;
6145 final FocusNode focusNode;
6146 final GlobalKey editableKey;
6147 final VoidCallback updateSelectionRects;
6148 final bool enabled;
6149
6150 @override
6151 _ScribbleFocusableState createState() => _ScribbleFocusableState();
6152 }
6153
6154 class _ScribbleFocusableState extends State<_ScribbleFocusable> implements ScribbleClient {
6155 _ScribbleFocusableState() : _elementIdentifier = (_nextElementIdentifier++).toString();
6156
6157 @override
6158 void initState() {
6159 super.initState();
6160 if (widget.enabled) {
6161 TextInput.registerScribbleElement(elementIdentifier, this);
6162 }
6163 }
6164
6165 @override
6166 void didUpdateWidget(_ScribbleFocusable oldWidget) {
6167 super.didUpdateWidget(oldWidget);
6168 if (!oldWidget.enabled && widget.enabled) {
6169 TextInput.registerScribbleElement(elementIdentifier, this);
6170 }
6171
6172 if (oldWidget.enabled && !widget.enabled) {
6173 TextInput.unregisterScribbleElement(elementIdentifier);
6174 }
6175 }
6176
6177 @override
6178 void dispose() {
6179 TextInput.unregisterScribbleElement(elementIdentifier);
6180 super.dispose();
6181 }
6182
6183 RenderEditable? get renderEditable =>
6184 widget.editableKey.currentContext?.findRenderObject() as RenderEditable?;
6185
6186 static int _nextElementIdentifier = 1;
6187 final String _elementIdentifier;
6188
6189 @override
6190 String get elementIdentifier => _elementIdentifier;
6191
6192 @override
6193 void onScribbleFocus(Offset offset) {
6194 widget.focusNode.requestFocus();
6195 renderEditable?.selectPositionAt(from: offset, cause: SelectionChangedCause.stylusHandwriting);
6196 widget.updateSelectionRects();
6197 }
6198
6199 @override
6200 bool isInScribbleRect(Rect rect) {
6201 final Rect calculatedBounds = bounds;
6202 if (renderEditable?.readOnly ?? false) {
6203 return false;
6204 }
6205 if (calculatedBounds == Rect.zero) {
6206 return false;
6207 }
6208 if (!calculatedBounds.overlaps(rect)) {
6209 return false;
6210 }
6211 final Rect intersection = calculatedBounds.intersect(rect);
6212 final HitTestResult result = HitTestResult();
6213 WidgetsBinding.instance.hitTestInView(result, intersection.center, View.of(context).viewId);
6214 return result.path.any((HitTestEntry entry) => entry.target == renderEditable);
6215 }
6216
6217 @override
6218 Rect get bounds {
6219 final RenderBox? box = context.findRenderObject() as RenderBox?;
6220 if (box == null || !mounted || !box.attached) {
6221 return Rect.zero;
6222 }
6223 final Matrix4 transform = box.getTransformTo(null);
6224 return MatrixUtils.transformRect(
6225 transform,
6226 Rect.fromLTWH(0, 0, box.size.width, box.size.height),
6227 );
6228 }
6229
6230 @override
6231 Widget build(BuildContext context) {
6232 return widget.child;
6233 }
6234 }
6235
6236 class _ScribblePlaceholder extends WidgetSpan {
6237 const _ScribblePlaceholder({required super.child, required this.size});
6238
6239 /// The size of the span, used in place of adding a placeholder size to the [TextPainter].
6240 final Size size;
6241
6242 @override
6243 void build(
6244 ui.ParagraphBuilder builder, {
6245 TextScaler textScaler = TextScaler.noScaling,
6246 List<PlaceholderDimensions>? dimensions,
6247 }) {
6248 assert(debugAssertIsValid());
6249 final bool hasStyle = style != null;
6250 if (hasStyle) {
6251 builder.pushStyle(style!.getTextStyle(textScaler: textScaler));
6252 }
6253 builder.addPlaceholder(size.width, size.height, alignment);
6254 if (hasStyle) {
6255 builder.pop();
6256 }
6257 }
6258 }
6259
6260 /// A text boundary that uses code points as logical boundaries.
6261 ///
6262 /// A code point represents a single character. This may be smaller than what is
6263 /// represented by a user-perceived character, or grapheme. For example, a
6264 /// single grapheme (in this case a Unicode extended grapheme cluster) like
6265 /// "👨👩👦" consists of five code points: the man emoji, a zero
6266 /// width joiner, the woman emoji, another zero width joiner, and the boy emoji.
6267 /// The [String] has a length of eight because each emoji consists of two code
6268 /// units.
6269 ///
6270 /// Code units are the units by which Dart's String class is measured, which is
6271 /// encoded in UTF-16.
6272 ///
6273 /// See also:
6274 ///
6275 /// * [String.runes], which deals with code points like this class.
6276 /// * [Characters], which deals with graphemes.
6277 /// * [CharacterBoundary], which is a [TextBoundary] like this class, but whose
6278 /// boundaries are graphemes instead of code points.
6279 class _CodePointBoundary extends TextBoundary {
6280 const _CodePointBoundary(this._text);
6281
6282 final String _text;
6283
6284 // Returns true if the given position falls in the center of a surrogate pair.
6285 bool _breaksSurrogatePair(int position) {
6286 assert(position > 0 && position < _text.length && _text.length > 1);
6287 return TextPainter.isHighSurrogate(_text.codeUnitAt(position - 1)) &&
6288 TextPainter.isLowSurrogate(_text.codeUnitAt(position));
6289 }
6290
6291 @override
6292 int? getLeadingTextBoundaryAt(int position) {
6293 if (_text.isEmpty || position < 0) {
6294 return null;
6295 }
6296 if (position == 0) {
6297 return 0;
6298 }
6299 if (position >= _text.length) {
6300 return _text.length;
6301 }
6302 if (_text.length <= 1) {
6303 return position;
6304 }
6305
6306 return _breaksSurrogatePair(position) ? position - 1 : position;
6307 }
6308
6309 @override
6310 int? getTrailingTextBoundaryAt(int position) {
6311 if (_text.isEmpty || position >= _text.length) {
6312 return null;
6313 }
6314 if (position < 0) {
6315 return 0;
6316 }
6317 if (position == _text.length - 1) {
6318 return _text.length;
6319 }
6320 if (_text.length <= 1) {
6321 return position;
6322 }
6323
6324 return _breaksSurrogatePair(position + 1) ? position + 2 : position + 1;
6325 }
6326 }
6327
6328 // ------------------------------- Text Actions -------------------------------
6329 class _DeleteTextAction<T extends DirectionalTextEditingIntent> extends ContextAction<T> {
6330 _DeleteTextAction(this.state, this.getTextBoundary, this._applyTextBoundary);
6331
6332 final EditableTextState state;
6333 final TextBoundary Function() getTextBoundary;
6334 final _ApplyTextBoundary _applyTextBoundary;
6335
6336 void _hideToolbarIfTextChanged(ReplaceTextIntent intent) {
6337 if (state._selectionOverlay == null || !state.selectionOverlay!.toolbarIsVisible) {
6338 return;
6339 }
6340 final TextEditingValue oldValue = intent.currentTextEditingValue;
6341 final TextEditingValue newValue = intent.currentTextEditingValue.replaced(
6342 intent.replacementRange,
6343 intent.replacementText,
6344 );
6345 if (oldValue.text != newValue.text) {
6346 // Hide the toolbar if the text was changed, but only hide the toolbar
6347 // overlay; the selection handle's visibility will be handled
6348 // by `_handleSelectionChanged`.
6349 state.hideToolbar(false);
6350 }
6351 }
6352
6353 @override
6354 Object? invoke(T intent, [BuildContext? context]) {
6355 final TextSelection selection = state._value.selection;
6356 if (!selection.isValid) {
6357 return null;
6358 }
6359 assert(selection.isValid);
6360 // Expands the selection to ensure the range covers full graphemes.
6361 final TextBoundary atomicBoundary = state._characterBoundary();
6362 if (!selection.isCollapsed) {
6363 // Expands the selection to ensure the range covers full graphemes.
6364 final TextRange range = TextRange(
6365 start: atomicBoundary.getLeadingTextBoundaryAt(selection.start) ?? state._value.text.length,
6366 end: atomicBoundary.getTrailingTextBoundaryAt(selection.end - 1) ?? 0,
6367 );
6368 final ReplaceTextIntent replaceTextIntent = ReplaceTextIntent(
6369 state._value,
6370 '',
6371 range,
6372 SelectionChangedCause.keyboard,
6373 );
6374 _hideToolbarIfTextChanged(replaceTextIntent);
6375 return Actions.invoke(context!, replaceTextIntent);
6376 }
6377
6378 final int target = _applyTextBoundary(selection.base, intent.forward, getTextBoundary()).offset;
6379
6380 final TextRange rangeToDelete = TextSelection(
6381 baseOffset: intent.forward
6382 ? atomicBoundary.getLeadingTextBoundaryAt(selection.baseOffset) ??
6383 state._value.text.length
6384 : atomicBoundary.getTrailingTextBoundaryAt(selection.baseOffset - 1) ?? 0,
6385 extentOffset: target,
6386 );
6387 final ReplaceTextIntent replaceTextIntent = ReplaceTextIntent(
6388 state._value,
6389 '',
6390 rangeToDelete,
6391 SelectionChangedCause.keyboard,
6392 );
6393 _hideToolbarIfTextChanged(replaceTextIntent);
6394 return Actions.invoke(context!, replaceTextIntent);
6395 }
6396
6397 @override
6398 bool get isActionEnabled => !state.widget.readOnly && state._value.selection.isValid;
6399 }
6400
6401 class _UpdateTextSelectionAction<T extends DirectionalCaretMovementIntent>
6402 extends ContextAction<T> {
6403 _UpdateTextSelectionAction(
6404 this.state,
6405 this.getTextBoundary,
6406 this.applyTextBoundary, {
6407 required this.ignoreNonCollapsedSelection,
6408 this.isExpand = false,
6409 this.extentAtIndex = false,
6410 });
6411
6412 final EditableTextState state;
6413 final bool ignoreNonCollapsedSelection;
6414 final bool isExpand;
6415 final bool extentAtIndex;
6416 final TextBoundary Function() getTextBoundary;
6417 final _ApplyTextBoundary applyTextBoundary;
6418
6419 static const int NEWLINE_CODE_UNIT = 10;
6420
6421 // Returns true iff the given position is at a wordwrap boundary in the
6422 // upstream position.
6423 bool _isAtWordwrapUpstream(TextPosition position) {
6424 final TextPosition end = TextPosition(
6425 offset: state.renderEditable.getLineAtOffset(position).end,
6426 affinity: TextAffinity.upstream,
6427 );
6428 return end == position &&
6429 end.offset != state.textEditingValue.text.length &&
6430 state.textEditingValue.text.codeUnitAt(position.offset) != NEWLINE_CODE_UNIT;
6431 }
6432
6433 // Returns true if the given position at a wordwrap boundary in the
6434 // downstream position.
6435 bool _isAtWordwrapDownstream(TextPosition position) {
6436 final TextPosition start = TextPosition(
6437 offset: state.renderEditable.getLineAtOffset(position).start,
6438 );
6439 return start == position &&
6440 start.offset != 0 &&
6441 state.textEditingValue.text.codeUnitAt(position.offset - 1) != NEWLINE_CODE_UNIT;
6442 }
6443
6444 @override
6445 Object? invoke(T intent, [BuildContext? context]) {
6446 final TextSelection selection = state._value.selection;
6447 assert(selection.isValid);
6448
6449 final bool collapseSelection = intent.collapseSelection || !state.widget.selectionEnabled;
6450 if (!selection.isCollapsed && !ignoreNonCollapsedSelection && collapseSelection) {
6451 return Actions.invoke(
6452 context!,
6453 UpdateSelectionIntent(
6454 state._value,
6455 TextSelection.collapsed(offset: intent.forward ? selection.end : selection.start),
6456 SelectionChangedCause.keyboard,
6457 ),
6458 );
6459 }
6460
6461 TextPosition extent = selection.extent;
6462 // If continuesAtWrap is true extent and is at the relevant wordwrap, then
6463 // move it just to the other side of the wordwrap.
6464 if (intent.continuesAtWrap) {
6465 if (intent.forward && _isAtWordwrapUpstream(extent)) {
6466 extent = TextPosition(offset: extent.offset);
6467 } else if (!intent.forward && _isAtWordwrapDownstream(extent)) {
6468 extent = TextPosition(offset: extent.offset, affinity: TextAffinity.upstream);
6469 }
6470 }
6471
6472 final bool shouldTargetBase =
6473 isExpand &&
6474 (intent.forward
6475 ? selection.baseOffset > selection.extentOffset
6476 : selection.baseOffset < selection.extentOffset);
6477 final TextPosition newExtent = applyTextBoundary(
6478 shouldTargetBase ? selection.base : extent,
6479 intent.forward,
6480 getTextBoundary(),
6481 );
6482 final TextSelection newSelection =
6483 collapseSelection || (!isExpand && newExtent.offset == selection.baseOffset)
6484 ? TextSelection.fromPosition(newExtent)
6485 : isExpand
6486 ? selection.expandTo(newExtent, extentAtIndex || selection.isCollapsed)
6487 : selection.extendTo(newExtent);
6488
6489 final bool shouldCollapseToBase =
6490 intent.collapseAtReversal &&
6491 (selection.baseOffset - selection.extentOffset) *
6492 (selection.baseOffset - newSelection.extentOffset) <
6493 0;
6494 final TextSelection newRange = shouldCollapseToBase
6495 ? TextSelection.fromPosition(selection.base)
6496 : newSelection;
6497 return Actions.invoke(
6498 context!,
6499 UpdateSelectionIntent(state._value, newRange, SelectionChangedCause.keyboard),
6500 );
6501 }
6502
6503 @override
6504 bool get isActionEnabled {
6505 if (kIsWeb && state.widget.selectionEnabled && state._value.composing.isValid) {
6506 return false;
6507 }
6508
6509 return state._value.selection.isValid;
6510 }
6511 }
6512
6513 class _UpdateTextSelectionVerticallyAction<T extends DirectionalCaretMovementIntent>
6514 extends ContextAction<T> {
6515 _UpdateTextSelectionVerticallyAction(this.state);
6516
6517 final EditableTextState state;
6518
6519 VerticalCaretMovementRun? _verticalMovementRun;
6520 TextSelection? _runSelection;
6521
6522 void stopCurrentVerticalRunIfSelectionChanges() {
6523 final TextSelection? runSelection = _runSelection;
6524 if (runSelection == null) {
6525 assert(_verticalMovementRun == null);
6526 return;
6527 }
6528 _runSelection = state._value.selection;
6529 final TextSelection currentSelection = state.widget.controller.selection;
6530 final bool continueCurrentRun =
6531 currentSelection.isValid &&
6532 currentSelection.isCollapsed &&
6533 currentSelection.baseOffset == runSelection.baseOffset &&
6534 currentSelection.extentOffset == runSelection.extentOffset;
6535 if (!continueCurrentRun) {
6536 _verticalMovementRun = null;
6537 _runSelection = null;
6538 }
6539 }
6540
6541 @override
6542 void invoke(T intent, [BuildContext? context]) {
6543 assert(state._value.selection.isValid);
6544
6545 final bool collapseSelection = intent.collapseSelection || !state.widget.selectionEnabled;
6546 final TextEditingValue value = state._textEditingValueforTextLayoutMetrics;
6547 if (!value.selection.isValid) {
6548 return;
6549 }
6550
6551 if (_verticalMovementRun?.isValid == false) {
6552 _verticalMovementRun = null;
6553 _runSelection = null;
6554 }
6555
6556 final VerticalCaretMovementRun currentRun =
6557 _verticalMovementRun ??
6558 state.renderEditable.startVerticalCaretMovement(state.renderEditable.selection!.extent);
6559
6560 final bool shouldMove = intent is ExtendSelectionVerticallyToAdjacentPageIntent
6561 ? currentRun.moveByOffset((intent.forward ? 1.0 : -1.0) * state.renderEditable.size.height)
6562 : intent.forward
6563 ? currentRun.moveNext()
6564 : currentRun.movePrevious();
6565 final TextPosition newExtent = shouldMove
6566 ? currentRun.current
6567 : intent.forward
6568 ? TextPosition(offset: value.text.length)
6569 : const TextPosition(offset: 0);
6570 final TextSelection newSelection = collapseSelection
6571 ? TextSelection.fromPosition(newExtent)
6572 : value.selection.extendTo(newExtent);
6573
6574 Actions.invoke(
6575 context!,
6576 UpdateSelectionIntent(value, newSelection, SelectionChangedCause.keyboard),
6577 );
6578 if (state._value.selection == newSelection) {
6579 _verticalMovementRun = currentRun;
6580 _runSelection = newSelection;
6581 }
6582 }
6583
6584 @override
6585 bool get isActionEnabled {
6586 if (kIsWeb && state.widget.selectionEnabled && state._value.composing.isValid) {
6587 return false;
6588 }
6589
6590 return state._value.selection.isValid;
6591 }
6592 }
6593
6594 class _WebComposingDisablingCallbackAction<T extends Intent> extends CallbackAction<T> {
6595 _WebComposingDisablingCallbackAction(this.state, {required super.onInvoke});
6596
6597 final EditableTextState state;
6598
6599 @override
6600 bool get isActionEnabled {
6601 if (kIsWeb && state.widget.selectionEnabled && state._value.composing.isValid) {
6602 return false;
6603 }
6604
6605 return super.isActionEnabled;
6606 }
6607 }
6608
6609 class _SelectAllAction extends ContextAction<SelectAllTextIntent> {
6610 _SelectAllAction(this.state);
6611
6612 final EditableTextState state;
6613
6614 @override
6615 Object? invoke(SelectAllTextIntent intent, [BuildContext? context]) {
6616 if (!state.widget.selectionEnabled) {
6617 return null;
6618 }
6619
6620 return Actions.invoke(
6621 context!,
6622 UpdateSelectionIntent(
6623 state._value,
6624 TextSelection(baseOffset: 0, extentOffset: state._value.text.length),
6625 intent.cause,
6626 ),
6627 );
6628 }
6629 }
6630
6631 class _CopySelectionAction extends ContextAction<CopySelectionTextIntent> {
6632 _CopySelectionAction(this.state);
6633
6634 final EditableTextState state;
6635
6636 @override
6637 void invoke(CopySelectionTextIntent intent, [BuildContext? context]) {
6638 if (!state._value.selection.isValid || state._value.selection.isCollapsed) {
6639 return;
6640 }
6641
6642 if (!state.widget.selectionEnabled) {
6643 return;
6644 }
6645
6646 if (intent.collapseSelection) {
6647 state.cutSelection(intent.cause);
6648 } else {
6649 state.copySelection(intent.cause);
6650 }
6651 }
6652 }
6653
6654 class _PasteSelectionAction extends ContextAction<PasteTextIntent> {
6655 _PasteSelectionAction(this.state);
6656
6657 final EditableTextState state;
6658
6659 @override
6660 void invoke(PasteTextIntent intent, [BuildContext? context]) {
6661 if (!state.widget.selectionEnabled) {
6662 return;
6663 }
6664
6665 state.pasteText(intent.cause);
6666 }
6667 }
6668
6669 /// A [ClipboardStatusNotifier] whose [value] is hardcoded to
6670 /// [ClipboardStatus.pasteable].
6671 ///
6672 /// Useful to avoid showing a permission dialog on web, which happens when
6673 /// [Clipboard.hasStrings] is called.
6674 class _WebClipboardStatusNotifier extends ClipboardStatusNotifier {
6675 @override
6676 ClipboardStatus value = ClipboardStatus.pasteable;
6677
6678 @override
6679 Future<void> update() {
6680 return Future<void>.value();
6681 }
6682 }
6683
6684 class _EditableTextTapOutsideAction extends ContextAction<EditableTextTapOutsideIntent> {
6685 _EditableTextTapOutsideAction();
6686
6687 @override
6688 void invoke(EditableTextTapOutsideIntent intent, [BuildContext? context]) {
6689 // The focus dropping behavior is only present on desktop platforms.
6690 switch (defaultTargetPlatform) {
6691 case TargetPlatform.android:
6692 case TargetPlatform.iOS:
6693 case TargetPlatform.fuchsia:
6694 // On mobile platforms, we don't unfocus on touch events unless they're
6695 // in the web browser, but we do unfocus for all other kinds of events.
6696 switch (intent.pointerDownEvent.kind) {
6697 case ui.PointerDeviceKind.touch:
6698 if (kIsWeb) {
6699 intent.focusNode.unfocus();
6700 }
6701 case ui.PointerDeviceKind.mouse:
6702 case ui.PointerDeviceKind.stylus:
6703 case ui.PointerDeviceKind.invertedStylus:
6704 case ui.PointerDeviceKind.unknown:
6705 intent.focusNode.unfocus();
6706 case ui.PointerDeviceKind.trackpad:
6707 throw UnimplementedError('Unexpected pointer down event for trackpad');
6708 }
6709 case TargetPlatform.linux:
6710 case TargetPlatform.macOS:
6711 case TargetPlatform.windows:
6712 intent.focusNode.unfocus();
6713 }
6714 }
6715 }
6716
6717 class _EditableTextTapUpOutsideAction extends ContextAction<EditableTextTapUpOutsideIntent> {
6718 _EditableTextTapUpOutsideAction();
6719
6720 @override
6721 void invoke(EditableTextTapUpOutsideIntent intent, [BuildContext? context]) {
6722 // The default action is a no-op.
6723 }
6724 }
6725