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