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