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

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com