text_field.dart 60.1 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle;

xster's avatar
xster committed
7
import 'package:flutter/cupertino.dart';
8
import 'package:flutter/foundation.dart';
9
import 'package:flutter/gestures.dart';
10 11
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
12

13
import 'adaptive_text_selection_toolbar.dart';
14
import 'color_scheme.dart';
15
import 'colors.dart';
16
import 'debug.dart';
17
import 'desktop_text_selection.dart';
18
import 'feedback.dart';
19
import 'input_decorator.dart';
20
import 'magnifier.dart';
21
import 'material_localizations.dart';
22
import 'material_state.dart';
23
import 'selectable_text.dart' show iOSHorizontalOffset;
24
import 'spell_check_suggestions_toolbar.dart';
25 26 27
import 'text_selection.dart';
import 'theme.dart';

28
export 'package:flutter/services.dart' show SmartDashesType, SmartQuotesType, TextCapitalization, TextInputAction, TextInputType;
29

30 31 32 33
// Examples can assume:
// late BuildContext context;
// late FocusNode myFocusNode;

34
/// Signature for the [TextField.buildCounter] callback.
35
typedef InputCounterWidgetBuilder = Widget? Function(
36
  /// The build context for the TextField.
37 38
  BuildContext context, {
  /// The length of the string currently in the input.
39
  required int currentLength,
40
  /// The maximum string length that can be entered into the TextField.
41
  required int? maxLength,
42
  /// Whether or not the TextField is currently focused. Mainly provided for
43
  /// the [liveRegion] parameter in the [Semantics] widget for accessibility.
44
  required bool isFocused,
45
});
46

47 48
class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDetectorBuilder {
  _TextFieldSelectionGestureDetectorBuilder({
49
    required _TextFieldState state,
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
  }) : _state = state,
       super(delegate: state);

  final _TextFieldState _state;

  @override
  void onForcePressStart(ForcePressDetails details) {
    super.onForcePressStart(details);
    if (delegate.selectionEnabled && shouldShowSelectionToolbar) {
      editableText.showToolbar();
    }
  }

  @override
  void onForcePressEnd(ForcePressDetails details) {
    // Not required.
  }

  @override
69
  void onSingleTapUp(TapDragUpDetails details) {
70
    super.onSingleTapUp(details);
71
    _state._requestKeyboard();
72
    _state.widget.onTap?.call();
73 74 75 76
  }

  @override
  void onSingleLongTapStart(LongPressStartDetails details) {
77
    super.onSingleLongTapStart(details);
78
    if (delegate.selectionEnabled) {
79
      switch (Theme.of(_state.context).platform) {
80
        case TargetPlatform.iOS:
81
        case TargetPlatform.macOS:
82 83 84
          break;
        case TargetPlatform.android:
        case TargetPlatform.fuchsia:
85 86
        case TargetPlatform.linux:
        case TargetPlatform.windows:
87 88 89 90 91 92
          Feedback.forLongPress(_state.context);
      }
    }
  }
}

93
/// A Material Design text field.
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
///
/// A text field lets the user enter text, either with hardware keyboard or with
/// an onscreen keyboard.
///
/// The text field calls the [onChanged] callback whenever the user changes the
/// text in the field. If the user indicates that they are done typing in the
/// field (e.g., by pressing a button on the soft keyboard), the text field
/// calls the [onSubmitted] callback.
///
/// To control the text that is displayed in the text field, use the
/// [controller]. For example, to set the initial value of the text field, use
/// a [controller] that already contains some text. The [controller] can also
/// control the selection and composing region (and to observe changes to the
/// text, selection, and composing region).
///
/// By default, a text field has a [decoration] that draws a divider below the
/// text field. You can use the [decoration] property to control the decoration,
/// for example by adding a label or an icon. If you set the [decoration]
/// property to null, the decoration will be removed entirely, including the
/// extra padding introduced by the decoration to save space for the labels.
///
/// If [decoration] is non-null (which is the default), the text field requires
116
/// one of its ancestors to be a [Material] widget.
117 118 119 120
///
/// To integrate the [TextField] into a [Form] with other [FormField] widgets,
/// consider using [TextFormField].
///
121 122 123 124 125 126 127
/// {@template flutter.material.textfield.wantKeepAlive}
/// When the widget has focus, it will prevent itself from disposing via its
/// underlying [EditableText]'s [AutomaticKeepAliveClientMixin.wantKeepAlive] in
/// order to avoid losing the selection. Removing the focus will allow it to be
/// disposed.
/// {@endtemplate}
///
128 129 130
/// Remember to call [TextEditingController.dispose] of the [TextEditingController]
/// when it is no longer needed. This will ensure we discard any resources used
/// by the object.
131
///
132 133 134
/// ## Obscured Input
///
/// {@tool dartpad}
135 136 137 138
/// This example shows how to create a [TextField] that will obscure input. The
/// [InputDecoration] surrounds the field in a border using [OutlineInputBorder]
/// and adds a label.
///
139
/// ** See code in examples/api/lib/material/text_field/text_field.0.dart **
140 141
/// {@end-tool}
///
142 143 144 145 146 147
/// ## Reading values
///
/// A common way to read a value from a TextField is to use the [onSubmitted]
/// callback. This callback is applied to the text field's current value when
/// the user finishes editing.
///
148
/// {@tool dartpad}
149 150 151
/// This sample shows how to get a value from a TextField via the [onSubmitted]
/// callback.
///
152
/// ** See code in examples/api/lib/material/text_field/text_field.1.dart **
153 154
/// {@end-tool}
///
155 156
/// {@macro flutter.widgets.EditableText.lifeCycle}
///
157 158 159 160 161 162 163 164 165 166 167
/// For most applications the [onSubmitted] callback will be sufficient for
/// reacting to user input.
///
/// The [onEditingComplete] callback also runs when the user finishes editing.
/// It's different from [onSubmitted] because it has a default value which
/// updates the text controller and yields the keyboard focus. Applications that
/// require different behavior can override the default [onEditingComplete]
/// callback.
///
/// Keep in mind you can also always read the current string from a TextField's
/// [TextEditingController] using [TextEditingController.text].
168
///
169
/// ## Handling emojis and other complex characters
170
/// {@macro flutter.widgets.EditableText.onChanged}
171 172 173 174 175
///
/// In the live Dartpad example above, try typing the emoji 👨‍👩‍👦
/// into the field and submitting. Because the example code measures the length
/// with `value.characters.length`, the emoji is correctly counted as a single
/// character.
176
///
177 178
/// {@macro flutter.widgets.editableText.showCaretOnScreen}
///
179 180
/// {@macro flutter.widgets.editableText.accessibility}
///
181 182 183 184 185 186
/// See also:
///
///  * [TextFormField], which integrates with the [Form] widget.
///  * [InputDecorator], which shows the labels and other visual elements that
///    surround the actual text editing widget.
///  * [EditableText], which is the raw text editing control at the heart of a
187
///    [TextField]. The [EditableText] widget is rarely used directly unless
188
///    you are implementing an entirely different design language, such as
189
///    Cupertino.
190 191 192 193 194
///  * <https://material.io/design/components/text-fields.html>
///  * Cookbook: [Create and style a text field](https://flutter.dev/docs/cookbook/forms/text-input)
///  * Cookbook: [Handle changes to a text field](https://flutter.dev/docs/cookbook/forms/text-field-changes)
///  * Cookbook: [Retrieve the value of a text field](https://flutter.dev/docs/cookbook/forms/retrieve-input)
///  * Cookbook: [Focus and text fields](https://flutter.dev/docs/cookbook/forms/focus)
195 196 197 198 199 200 201 202 203
class TextField extends StatefulWidget {
  /// Creates a Material Design text field.
  ///
  /// If [decoration] is non-null (which is the default), the text field requires
  /// one of its ancestors to be a [Material] widget.
  ///
  /// To remove the decoration entirely (including the extra padding introduced
  /// by the decoration to save space for the labels), set the [decoration] to
  /// null.
204 205
  ///
  /// The [maxLines] property can be set to null to remove the restriction on
206
  /// the number of lines. By default, it is one, meaning this is a single-line
207
  /// text field. [maxLines] must not be zero.
208 209 210
  ///
  /// The [maxLength] property is set to null by default, which means the
  /// number of characters allowed in the text field is not restricted. If
211 212 213
  /// [maxLength] is set a character counter will be displayed below the
  /// field showing how many characters have been entered. If the value is
  /// set to a positive integer it will also display the maximum allowed
214
  /// number of characters to be entered. If the value is set to
215
  /// [TextField.noMaxLength] then only the current length is displayed.
216 217
  ///
  /// After [maxLength] characters have been input, additional input
218 219 220 221 222
  /// is ignored, unless [maxLengthEnforcement] is set to
  /// [MaxLengthEnforcement.none].
  /// The text field enforces the length with a [LengthLimitingTextInputFormatter],
  /// which is evaluated after the supplied [inputFormatters], if any.
  /// The [maxLength] value must be either null or greater than zero.
223
  ///
224 225 226 227
  /// If [maxLengthEnforcement] is set to [MaxLengthEnforcement.none], then more
  /// than [maxLength] characters may be entered, and the error counter and
  /// divider will switch to the [decoration].errorStyle when the limit is
  /// exceeded.
228
  ///
229 230 231
  /// The text cursor is not shown if [showCursor] is false or if [showCursor]
  /// is null (the default) and [readOnly] is true.
  ///
232 233 234 235 236
  /// The [selectionHeightStyle] and [selectionWidthStyle] properties allow
  /// changing the shape of the selection highlighting. These properties default
  /// to [ui.BoxHeightStyle.tight] and [ui.BoxWidthStyle.tight] respectively and
  /// must not be null.
  ///
237
  /// The [textAlign], [autofocus], [obscureText], [readOnly], [autocorrect],
238 239
  /// [scrollPadding], [maxLines], [maxLength], [selectionHeightStyle],
  /// [selectionWidthStyle], [enableSuggestions], and
240
  /// [enableIMEPersonalizedLearning] arguments must not be null.
241 242 243 244 245
  ///
  /// See also:
  ///
  ///  * [maxLength], which discusses the precise meaning of "number of
  ///    characters" and how it may differ from the intuitive meaning.
246
  const TextField({
247
    super.key,
248 249
    this.controller,
    this.focusNode,
250
    this.undoController,
251
    this.decoration = const InputDecoration(),
252
    TextInputType? keyboardType,
253
    this.textInputAction,
254
    this.textCapitalization = TextCapitalization.none,
255
    this.style,
256
    this.strutStyle,
257
    this.textAlign = TextAlign.start,
258
    this.textAlignVertical,
259
    this.textDirection,
260
    this.readOnly = false,
261 262 263 264 265
    @Deprecated(
      'Use `contextMenuBuilder` instead. '
      'This feature was deprecated after v3.3.0-0.5.pre.',
    )
    this.toolbarOptions,
266
    this.showCursor,
267
    this.autofocus = false,
268
    this.obscuringCharacter = '•',
269 270
    this.obscureText = false,
    this.autocorrect = true,
271 272
    SmartDashesType? smartDashesType,
    SmartQuotesType? smartQuotesType,
273
    this.enableSuggestions = true,
274
    this.maxLines = 1,
275 276
    this.minLines,
    this.expands = false,
277
    this.maxLength,
278
    this.maxLengthEnforcement,
279
    this.onChanged,
280
    this.onEditingComplete,
281
    this.onSubmitted,
282
    this.onAppPrivateCommand,
283
    this.inputFormatters,
284
    this.enabled,
285
    this.cursorWidth = 2.0,
286
    this.cursorHeight,
287
    this.cursorRadius,
288
    this.cursorOpacityAnimates,
289
    this.cursorColor,
290 291
    this.selectionHeightStyle = ui.BoxHeightStyle.tight,
    this.selectionWidthStyle = ui.BoxWidthStyle.tight,
292
    this.keyboardAppearance,
293
    this.scrollPadding = const EdgeInsets.all(20.0),
294
    this.dragStartBehavior = DragStartBehavior.start,
295
    bool? enableInteractiveSelection,
296
    this.selectionControls,
297
    this.onTap,
298
    this.onTapOutside,
299
    this.mouseCursor,
300
    this.buildCounter,
301
    this.scrollController,
302
    this.scrollPhysics,
303
    this.autofillHints = const <String>[],
304
    this.contentInsertionConfiguration,
305
    this.clipBehavior = Clip.hardEdge,
306
    this.restorationId,
307
    this.scribbleEnabled = true,
308
    this.enableIMEPersonalizedLearning = true,
309
    this.contextMenuBuilder = _defaultContextMenuBuilder,
310
    this.canRequestFocus = true,
311
    this.spellCheckConfiguration,
312
    this.magnifierConfiguration,
313
  }) : assert(obscuringCharacter.length == 1),
314 315
       smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
       smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
316
       assert(maxLines == null || maxLines > 0),
317 318 319
       assert(minLines == null || minLines > 0),
       assert(
         (maxLines == null) || (minLines == null) || (maxLines >= minLines),
320
         "minLines can't be greater than maxLines",
321 322 323 324 325
       ),
       assert(
         !expands || (maxLines == null && minLines == null),
         'minLines and maxLines must be null when expands is true.',
       ),
326
       assert(!obscureText || maxLines == 1, 'Obscured fields cannot be multiline.'),
327
       assert(maxLength == null || maxLength == TextField.noMaxLength || maxLength > 0),
328
       // Assert the following instead of setting it directly to avoid surprising the user by silently changing the value they set.
329 330
       assert(
         !identical(textInputAction, TextInputAction.newline) ||
331 332
         maxLines == 1 ||
         !identical(keyboardType, TextInputType.text),
333 334
         'Use keyboardType TextInputType.multiline when using TextInputAction.newline on a multiline TextField.',
       ),
335
       keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
336
       enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText);
337

338
  /// {@macro flutter.widgets.magnifier.TextMagnifierConfiguration.intro}
339 340 341
  ///
  /// {@macro flutter.widgets.magnifier.intro}
  ///
342
  /// {@macro flutter.widgets.magnifier.TextMagnifierConfiguration.details}
343
  ///
344 345 346
  /// By default, builds a [CupertinoTextMagnifier] on iOS and [TextMagnifier]
  /// on Android, and builds nothing on all other platforms. If it is desired to
  /// suppress the magnifier, consider passing [TextMagnifierConfiguration.disabled].
347 348 349 350 351 352
  ///
  /// {@tool dartpad}
  /// This sample demonstrates how to customize the magnifier that this text field uses.
  ///
  /// ** See code in examples/api/lib/widgets/text_magnifier/text_magnifier.0.dart **
  /// {@end-tool}
353 354
  final TextMagnifierConfiguration? magnifierConfiguration;

355 356
  /// Controls the text being edited.
  ///
357
  /// If null, this widget will create its own [TextEditingController].
358
  final TextEditingController? controller;
359

360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377
  /// Defines the keyboard focus for this widget.
  ///
  /// The [focusNode] is a long-lived object that's typically managed by a
  /// [StatefulWidget] parent. See [FocusNode] for more information.
  ///
  /// To give the keyboard focus to this widget, provide a [focusNode] and then
  /// use the current [FocusScope] to request the focus:
  ///
  /// ```dart
  /// FocusScope.of(context).requestFocus(myFocusNode);
  /// ```
  ///
  /// This happens automatically when the widget is tapped.
  ///
  /// To be notified when the widget gains or loses the focus, add a listener
  /// to the [focusNode]:
  ///
  /// ```dart
378
  /// myFocusNode.addListener(() { print(myFocusNode.hasFocus); });
379
  /// ```
380 381
  ///
  /// If null, this widget will create its own [FocusNode].
382 383 384
  ///
  /// ## Keyboard
  ///
Gary Qian's avatar
Gary Qian committed
385
  /// Requesting the focus will typically cause the keyboard to be shown
386 387
  /// if it's not showing already.
  ///
388
  /// On Android, the user can hide the keyboard - without changing the focus -
389
  /// with the system back button. They can restore the keyboard's visibility
390
  /// by tapping on a text field. The user might hide the keyboard and
391 392 393 394 395 396 397
  /// switch to a physical keyboard, or they might just need to get it
  /// out of the way for a moment, to expose something it's
  /// obscuring. In this case requesting the focus again will not
  /// cause the focus to change, and will not make the keyboard visible.
  ///
  /// This widget builds an [EditableText] and will ensure that the keyboard is
  /// showing when it is tapped by calling [EditableTextState.requestKeyboard()].
398
  final FocusNode? focusNode;
399 400 401

  /// The decoration to show around the text field.
  ///
402
  /// By default, draws a horizontal line under the text field but can be
403 404
  /// configured to show an icon, label, hint text, and error text.
  ///
405
  /// Specify null to remove the decoration entirely (including the
406
  /// extra padding introduced by the decoration to save space for the labels).
407
  final InputDecoration? decoration;
408

409
  /// {@macro flutter.widgets.editableText.keyboardType}
410 411
  final TextInputType keyboardType;

412 413
  /// The type of action button to use for the keyboard.
  ///
414 415
  /// Defaults to [TextInputAction.newline] if [keyboardType] is
  /// [TextInputType.multiline] and [TextInputAction.done] otherwise.
416
  final TextInputAction? textInputAction;
417

418
  /// {@macro flutter.widgets.editableText.textCapitalization}
419 420
  final TextCapitalization textCapitalization;

421 422 423 424
  /// The style to use for the text being edited.
  ///
  /// This text style is also used as the base style for the [decoration].
  ///
425
  /// If null, defaults to the `titleMedium` text style from the current [Theme].
426
  final TextStyle? style;
427

428
  /// {@macro flutter.widgets.editableText.strutStyle}
429
  final StrutStyle? strutStyle;
430

431
  /// {@macro flutter.widgets.editableText.textAlign}
432 433
  final TextAlign textAlign;

434
  /// {@macro flutter.material.InputDecorator.textAlignVertical}
435
  final TextAlignVertical? textAlignVertical;
436

437
  /// {@macro flutter.widgets.editableText.textDirection}
438
  final TextDirection? textDirection;
439

440
  /// {@macro flutter.widgets.editableText.autofocus}
441 442
  final bool autofocus;

443 444 445
  /// {@macro flutter.widgets.editableText.obscuringCharacter}
  final String obscuringCharacter;

446
  /// {@macro flutter.widgets.editableText.obscureText}
447 448
  final bool obscureText;

449
  /// {@macro flutter.widgets.editableText.autocorrect}
450 451
  final bool autocorrect;

452
  /// {@macro flutter.services.TextInputConfiguration.smartDashesType}
453 454
  final SmartDashesType smartDashesType;

455
  /// {@macro flutter.services.TextInputConfiguration.smartQuotesType}
456 457
  final SmartQuotesType smartQuotesType;

458
  /// {@macro flutter.services.TextInputConfiguration.enableSuggestions}
459 460
  final bool enableSuggestions;

461
  /// {@macro flutter.widgets.editableText.maxLines}
462 463
  ///  * [expands], which determines whether the field should fill the height of
  ///    its parent.
464
  final int? maxLines;
465

466
  /// {@macro flutter.widgets.editableText.minLines}
467 468
  ///  * [expands], which determines whether the field should fill the height of
  ///    its parent.
469
  final int? minLines;
470 471 472 473

  /// {@macro flutter.widgets.editableText.expands}
  final bool expands;

474 475 476
  /// {@macro flutter.widgets.editableText.readOnly}
  final bool readOnly;

477 478 479 480 481
  /// Configuration of toolbar options.
  ///
  /// If not set, select all and paste will default to be enabled. Copy and cut
  /// will be disabled if [obscureText] is true. If [readOnly] is true,
  /// paste and cut will be disabled regardless.
482 483 484 485 486
  @Deprecated(
    'Use `contextMenuBuilder` instead. '
    'This feature was deprecated after v3.3.0-0.5.pre.',
  )
  final ToolbarOptions? toolbarOptions;
487

488
  /// {@macro flutter.widgets.editableText.showCursor}
489
  final bool? showCursor;
490

491 492
  /// If [maxLength] is set to this value, only the "current input length"
  /// part of the character counter is shown.
493
  static const int noMaxLength = -1;
494

495 496
  /// The maximum number of characters (Unicode grapheme clusters) to allow in
  /// the text field.
497 498
  ///
  /// If set, a character counter will be displayed below the
499
  /// field showing how many characters have been entered. If set to a number
500
  /// greater than 0, it will also display the maximum number allowed. If set
501 502 503
  /// to [TextField.noMaxLength] then only the current character count is displayed.
  ///
  /// After [maxLength] characters have been input, additional input
504 505 506 507 508
  /// is ignored, unless [maxLengthEnforcement] is set to
  /// [MaxLengthEnforcement.none].
  ///
  /// The text field enforces the length with a [LengthLimitingTextInputFormatter],
  /// which is evaluated after the supplied [inputFormatters], if any.
509
  ///
510 511 512 513
  /// This value must be either null, [TextField.noMaxLength], or greater than 0.
  /// If null (the default) then there is no limit to the number of characters
  /// that can be entered. If set to [TextField.noMaxLength], then no limit will
  /// be enforced, but the number of characters entered will still be displayed.
514 515 516 517
  ///
  /// Whitespace characters (e.g. newline, space, tab) are included in the
  /// character count.
  ///
518 519 520 521
  /// If [maxLengthEnforcement] is [MaxLengthEnforcement.none], then more than
  /// [maxLength] characters may be entered, but the error counter and divider
  /// will switch to the [decoration]'s [InputDecoration.errorStyle] when the
  /// limit is exceeded.
522
  ///
523
  /// {@macro flutter.services.lengthLimitingTextInputFormatter.maxLength}
524
  final int? maxLength;
525

526 527 528 529 530 531 532
  /// Determines how the [maxLength] limit should be enforced.
  ///
  /// {@macro flutter.services.textFormatter.effectiveMaxLengthEnforcement}
  ///
  /// {@macro flutter.services.textFormatter.maxLengthEnforcement}
  final MaxLengthEnforcement? maxLengthEnforcement;

533
  /// {@macro flutter.widgets.editableText.onChanged}
534 535 536 537 538
  ///
  /// See also:
  ///
  ///  * [inputFormatters], which are called before [onChanged]
  ///    runs and can validate and change ("format") the input value.
539
  ///  * [onEditingComplete], [onSubmitted]:
540
  ///    which are more specialized input change notifications.
541
  final ValueChanged<String>? onChanged;
542

543
  /// {@macro flutter.widgets.editableText.onEditingComplete}
544
  final VoidCallback? onEditingComplete;
545

546
  /// {@macro flutter.widgets.editableText.onSubmitted}
547 548 549
  ///
  /// See also:
  ///
550 551 552
  ///  * [TextInputAction.next] and [TextInputAction.previous], which
  ///    automatically shift the focus to the next/previous focusable item when
  ///    the user is done editing.
553
  final ValueChanged<String>? onSubmitted;
554

555
  /// {@macro flutter.widgets.editableText.onAppPrivateCommand}
556
  final AppPrivateCommandCallback? onAppPrivateCommand;
557

558
  /// {@macro flutter.widgets.editableText.inputFormatters}
559
  final List<TextInputFormatter>? inputFormatters;
560

561
  /// If false the text field is "disabled": it ignores taps and its
562 563 564
  /// [decoration] is rendered in grey.
  ///
  /// If non-null this property overrides the [decoration]'s
565
  /// [InputDecoration.enabled] property.
566
  final bool? enabled;
567

568
  /// {@macro flutter.widgets.editableText.cursorWidth}
569 570
  final double cursorWidth;

571
  /// {@macro flutter.widgets.editableText.cursorHeight}
572
  final double? cursorHeight;
573

574
  /// {@macro flutter.widgets.editableText.cursorRadius}
575
  final Radius? cursorRadius;
576

577 578 579
  /// {@macro flutter.widgets.editableText.cursorOpacityAnimates}
  final bool? cursorOpacityAnimates;

580
  /// The color of the cursor.
581
  ///
582 583 584
  /// The cursor indicates the current location of text insertion point in
  /// the field.
  ///
585
  /// If this is null it will default to the ambient
586
  /// [DefaultSelectionStyle.cursorColor]. If that is null, and the
587 588 589
  /// [ThemeData.platform] is [TargetPlatform.iOS] or [TargetPlatform.macOS]
  /// it will use [CupertinoThemeData.primaryColor]. Otherwise it will use
  /// the value of [ColorScheme.primary] of [ThemeData.colorScheme].
590
  final Color? cursorColor;
591

592 593 594 595 596 597 598 599 600 601
  /// Controls how tall the selection highlight boxes are computed to be.
  ///
  /// See [ui.BoxHeightStyle] for details on available styles.
  final ui.BoxHeightStyle selectionHeightStyle;

  /// Controls how wide the selection highlight boxes are computed to be.
  ///
  /// See [ui.BoxWidthStyle] for details on available styles.
  final ui.BoxWidthStyle selectionWidthStyle;

602
  /// The appearance of the keyboard.
603
  ///
604
  /// This setting is only honored on iOS devices.
605
  ///
606
  /// If unset, defaults to [ThemeData.brightness].
607
  final Brightness? keyboardAppearance;
608

609
  /// {@macro flutter.widgets.editableText.scrollPadding}
610 611
  final EdgeInsets scrollPadding;

612 613 614
  /// {@macro flutter.widgets.editableText.enableInteractiveSelection}
  final bool enableInteractiveSelection;

615 616 617
  /// {@macro flutter.widgets.editableText.selectionControls}
  final TextSelectionControls? selectionControls;

618 619 620
  /// {@macro flutter.widgets.scrollable.dragStartBehavior}
  final DragStartBehavior dragStartBehavior;

621
  /// {@macro flutter.widgets.editableText.selectionEnabled}
622
  bool get selectionEnabled => enableInteractiveSelection;
623

624 625
  /// {@template flutter.material.textfield.onTap}
  /// Called for each distinct tap except for every second tap of a double tap.
626
  ///
627
  /// The text field builds a [GestureDetector] to handle input events like tap,
628
  /// to trigger focus requests, to move the caret, adjust the selection, etc.
629
  /// Handling some of those events by wrapping the text field with a competing
630 631
  /// GestureDetector is problematic.
  ///
632
  /// To unconditionally handle taps, without interfering with the text field's
633 634
  /// internal gesture detector, provide this callback.
  ///
635
  /// If the text field is created with [enabled] false, taps will not be
636 637
  /// recognized.
  ///
638
  /// To be notified when the text field gains or loses the focus, provide a
639 640 641
  /// [focusNode] and add a listener to that.
  ///
  /// To listen to arbitrary pointer events without competing with the
642
  /// text field's internal gesture detector, use a [Listener].
643
  /// {@endtemplate}
644
  final GestureTapCallback? onTap;
645

646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663
  /// {@macro flutter.widgets.editableText.onTapOutside}
  ///
  /// {@tool dartpad}
  /// This example shows how to use a `TextFieldTapRegion` to wrap a set of
  /// "spinner" buttons that increment and decrement a value in the [TextField]
  /// without causing the text field to lose keyboard focus.
  ///
  /// This example includes a generic `SpinnerField<T>` class that you can copy
  /// into your own project and customize.
  ///
  /// ** See code in examples/api/lib/widgets/tap_region/text_field_tap_region.0.dart **
  /// {@end-tool}
  ///
  /// See also:
  ///
  ///  * [TapRegion] for how the region group is determined.
  final TapRegionCallback? onTapOutside;

664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680
  /// The cursor for a mouse pointer when it enters or is hovering over the
  /// widget.
  ///
  /// If [mouseCursor] is a [MaterialStateProperty<MouseCursor>],
  /// [MaterialStateProperty.resolve] is used for the following [MaterialState]s:
  ///
  ///  * [MaterialState.error].
  ///  * [MaterialState.hovered].
  ///  * [MaterialState.focused].
  ///  * [MaterialState.disabled].
  ///
  /// If this property is null, [MaterialStateMouseCursor.textable] will be used.
  ///
  /// The [mouseCursor] is the only property of [TextField] that controls the
  /// appearance of the mouse pointer. All other properties related to "cursor"
  /// stand for the text cursor, which is usually a blinking vertical line at
  /// the editing position.
681
  final MouseCursor? mouseCursor;
682

683
  /// Callback that generates a custom [InputDecoration.counter] widget.
684 685
  ///
  /// See [InputCounterWidgetBuilder] for an explanation of the passed in
686
  /// arguments. The returned widget will be placed below the line in place of
687
  /// the default widget built when [InputDecoration.counterText] is specified.
688 689
  ///
  /// The returned widget will be wrapped in a [Semantics] widget for
690 691
  /// accessibility, but it also needs to be accessible itself. For example,
  /// if returning a Text widget, set the [Text.semanticsLabel] property.
692
  ///
693
  /// {@tool snippet}
694 695 696 697
  /// ```dart
  /// Widget counter(
  ///   BuildContext context,
  ///   {
698 699 700
  ///     required int currentLength,
  ///     required int? maxLength,
  ///     required bool isFocused,
701 702 703 704 705 706 707 708 709
  ///   }
  /// ) {
  ///   return Text(
  ///     '$currentLength of $maxLength characters',
  ///     semanticsLabel: 'character count',
  ///   );
  /// }
  /// ```
  /// {@end-tool}
710 711 712
  ///
  /// If buildCounter returns null, then no counter and no Semantics widget will
  /// be created at all.
713
  final InputCounterWidgetBuilder? buildCounter;
714

Dan Field's avatar
Dan Field committed
715
  /// {@macro flutter.widgets.editableText.scrollPhysics}
716
  final ScrollPhysics? scrollPhysics;
717

718
  /// {@macro flutter.widgets.editableText.scrollController}
719
  final ScrollController? scrollController;
720

721
  /// {@macro flutter.widgets.editableText.autofillHints}
722
  /// {@macro flutter.services.AutofillConfiguration.autofillHints}
723
  final Iterable<String>? autofillHints;
724

725 726 727 728 729
  /// {@macro flutter.material.Material.clipBehavior}
  ///
  /// Defaults to [Clip.hardEdge].
  final Clip clipBehavior;

730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746
  /// {@template flutter.material.textfield.restorationId}
  /// Restoration ID to save and restore the state of the text field.
  ///
  /// If non-null, the text field will persist and restore its current scroll
  /// offset and - if no [controller] has been provided - the content of the
  /// text field. If a [controller] has been provided, it is the responsibility
  /// of the owner of that controller to persist and restore it, e.g. by using
  /// a [RestorableTextEditingController].
  ///
  /// The state of this widget is persisted in a [RestorationBucket] claimed
  /// from the surrounding [RestorationScope] using the provided restoration ID.
  ///
  /// See also:
  ///
  ///  * [RestorationManager], which explains how state restoration works in
  ///    Flutter.
  /// {@endtemplate}
747
  final String? restorationId;
748

749 750 751
  /// {@macro flutter.widgets.editableText.scribbleEnabled}
  final bool scribbleEnabled;

752 753 754
  /// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
  final bool enableIMEPersonalizedLearning;

755 756 757
  /// {@macro flutter.widgets.editableText.contentInsertionConfiguration}
  final ContentInsertionConfiguration? contentInsertionConfiguration;

758 759 760 761 762 763 764 765 766
  /// {@macro flutter.widgets.EditableText.contextMenuBuilder}
  ///
  /// If not provided, will build a default menu based on the platform.
  ///
  /// See also:
  ///
  ///  * [AdaptiveTextSelectionToolbar], which is built by default.
  final EditableTextContextMenuBuilder? contextMenuBuilder;

767 768 769 770 771 772 773
  /// Determine whether this text field can request the primary focus.
  ///
  /// Defaults to true. If false, the text field will not request focus
  /// when tapped, or when its context menu is displayed. If false it will not
  /// be possible to move the focus to the text field with tab key.
  final bool canRequestFocus;

774 775 776
  /// {@macro flutter.widgets.undoHistory.controller}
  final UndoHistoryController? undoController;

777 778 779 780 781 782
  static Widget _defaultContextMenuBuilder(BuildContext context, EditableTextState editableTextState) {
    return AdaptiveTextSelectionToolbar.editableText(
      editableTextState: editableTextState,
    );
  }

783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802
  /// {@macro flutter.widgets.EditableText.spellCheckConfiguration}
  ///
  /// If [SpellCheckConfiguration.misspelledTextStyle] is not specified in this
  /// configuration, then [materialMisspelledTextStyle] is used by default.
  final SpellCheckConfiguration? spellCheckConfiguration;

  /// The [TextStyle] used to indicate misspelled words in the Material style.
  ///
  /// See also:
  ///  * [SpellCheckConfiguration.misspelledTextStyle], the style configured to
  ///    mark misspelled words with.
  ///  * [CupertinoTextField.cupertinoMisspelledTextStyle], the style configured
  ///    to mark misspelled words with in the Cupertino style.
  static const TextStyle materialMisspelledTextStyle =
    TextStyle(
      decoration: TextDecoration.underline,
      decorationColor: Colors.red,
      decorationStyle: TextDecorationStyle.wavy,
  );

803 804 805 806 807
  /// Default builder for the spell check suggestions toolbar in the Material
  /// style.
  ///
  /// See also:
  ///  * [SpellCheckConfiguration.spellCheckSuggestionsToolbarBuilder], the
808 809 810
  ///    builder configured to show a spell check suggestions toolbar.
  ///  * [CupertinoTextField.defaultSpellCheckSuggestionsToolbarBuilder], the builder
  ///    configured to show the Material style spell check suggestions toolbar.
811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830
  @visibleForTesting
  static Widget defaultSpellCheckSuggestionsToolbarBuilder(
    BuildContext context,
    EditableTextState editableTextState,
  ) {
    final Offset anchor =
      SpellCheckSuggestionsToolbar.getToolbarAnchor(editableTextState.contextMenuAnchors);
    final List<ContextMenuButtonItem>? buttonItems =
      SpellCheckSuggestionsToolbar.buildButtonItems(context, editableTextState);

    if (buttonItems == null){
      return const SizedBox.shrink();
    }

    return SpellCheckSuggestionsToolbar(
      anchor: anchor,
      buttonItems: buttonItems,
    );
  }

831
  @override
832
  State<TextField> createState() => _TextFieldState();
833 834

  @override
835 836
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
837 838
    properties.add(DiagnosticsProperty<TextEditingController>('controller', controller, defaultValue: null));
    properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode, defaultValue: null));
839
    properties.add(DiagnosticsProperty<UndoHistoryController>('undoController', undoController, defaultValue: null));
840
    properties.add(DiagnosticsProperty<bool>('enabled', enabled, defaultValue: null));
841
    properties.add(DiagnosticsProperty<InputDecoration>('decoration', decoration, defaultValue: const InputDecoration()));
842 843 844
    properties.add(DiagnosticsProperty<TextInputType>('keyboardType', keyboardType, defaultValue: TextInputType.text));
    properties.add(DiagnosticsProperty<TextStyle>('style', style, defaultValue: null));
    properties.add(DiagnosticsProperty<bool>('autofocus', autofocus, defaultValue: false));
845
    properties.add(DiagnosticsProperty<String>('obscuringCharacter', obscuringCharacter, defaultValue: '•'));
846
    properties.add(DiagnosticsProperty<bool>('obscureText', obscureText, defaultValue: false));
847
    properties.add(DiagnosticsProperty<bool>('autocorrect', autocorrect, defaultValue: true));
848 849
    properties.add(EnumProperty<SmartDashesType>('smartDashesType', smartDashesType, defaultValue: obscureText ? SmartDashesType.disabled : SmartDashesType.enabled));
    properties.add(EnumProperty<SmartQuotesType>('smartQuotesType', smartQuotesType, defaultValue: obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled));
850
    properties.add(DiagnosticsProperty<bool>('enableSuggestions', enableSuggestions, defaultValue: true));
851
    properties.add(IntProperty('maxLines', maxLines, defaultValue: 1));
852 853
    properties.add(IntProperty('minLines', minLines, defaultValue: null));
    properties.add(DiagnosticsProperty<bool>('expands', expands, defaultValue: false));
854
    properties.add(IntProperty('maxLength', maxLength, defaultValue: null));
855
    properties.add(EnumProperty<MaxLengthEnforcement>('maxLengthEnforcement', maxLengthEnforcement, defaultValue: null));
856 857 858
    properties.add(EnumProperty<TextInputAction>('textInputAction', textInputAction, defaultValue: null));
    properties.add(EnumProperty<TextCapitalization>('textCapitalization', textCapitalization, defaultValue: TextCapitalization.none));
    properties.add(EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: TextAlign.start));
859
    properties.add(DiagnosticsProperty<TextAlignVertical>('textAlignVertical', textAlignVertical, defaultValue: null));
860 861
    properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
    properties.add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0));
862
    properties.add(DoubleProperty('cursorHeight', cursorHeight, defaultValue: null));
863
    properties.add(DiagnosticsProperty<Radius>('cursorRadius', cursorRadius, defaultValue: null));
864
    properties.add(DiagnosticsProperty<bool>('cursorOpacityAnimates', cursorOpacityAnimates, defaultValue: null));
865
    properties.add(ColorProperty('cursorColor', cursorColor, defaultValue: null));
866 867 868
    properties.add(DiagnosticsProperty<Brightness>('keyboardAppearance', keyboardAppearance, defaultValue: null));
    properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('scrollPadding', scrollPadding, defaultValue: const EdgeInsets.all(20.0)));
    properties.add(FlagProperty('selectionEnabled', value: selectionEnabled, defaultValue: true, ifFalse: 'selection disabled'));
869
    properties.add(DiagnosticsProperty<TextSelectionControls>('selectionControls', selectionControls, defaultValue: null));
870
    properties.add(DiagnosticsProperty<ScrollController>('scrollController', scrollController, defaultValue: null));
871
    properties.add(DiagnosticsProperty<ScrollPhysics>('scrollPhysics', scrollPhysics, defaultValue: null));
872
    properties.add(DiagnosticsProperty<Clip>('clipBehavior', clipBehavior, defaultValue: Clip.hardEdge));
873
    properties.add(DiagnosticsProperty<bool>('scribbleEnabled', scribbleEnabled, defaultValue: true));
874
    properties.add(DiagnosticsProperty<bool>('enableIMEPersonalizedLearning', enableIMEPersonalizedLearning, defaultValue: true));
875
    properties.add(DiagnosticsProperty<SpellCheckConfiguration>('spellCheckConfiguration', spellCheckConfiguration, defaultValue: null));
876
    properties.add(DiagnosticsProperty<List<String>>('contentCommitMimeTypes', contentInsertionConfiguration?.allowedMimeTypes ?? const <String>[], defaultValue: contentInsertionConfiguration == null ? const <String>[] : kDefaultContentInsertionMimeTypes));
877 878 879
  }
}

880
class _TextFieldState extends State<TextField> with RestorationMixin implements TextSelectionGestureDetectorBuilderDelegate, AutofillClient {
881 882
  RestorableTextEditingController? _controller;
  TextEditingController get _effectiveController => widget.controller ?? _controller!.value;
883

884
  FocusNode? _focusNode;
885
  FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode());
886

887
  MaxLengthEnforcement get _effectiveMaxLengthEnforcement => widget.maxLengthEnforcement
888
    ?? LengthLimitingTextInputFormatter.getDefaultMaxLengthEnforcement(Theme.of(context).platform);
889

890 891
  bool _isHovering = false;

892 893
  bool get needsCounter => widget.maxLength != null
    && widget.decoration != null
894
    && widget.decoration!.counterText == null;
895

896 897
  bool _showSelectionHandles = false;

898
  late _TextFieldSelectionGestureDetectorBuilder _selectionGestureDetectorBuilder;
899 900 901

  // API for TextSelectionGestureDetectorBuilderDelegate.
  @override
902
  late bool forcePressEnabled;
903 904 905 906 907 908 909 910

  @override
  final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>();

  @override
  bool get selectionEnabled => widget.selectionEnabled;
  // End of API for TextSelectionGestureDetectorBuilderDelegate.

911 912
  bool get _isEnabled =>  widget.enabled ?? widget.decoration?.enabled ?? true;

913
  int get _currentLength => _effectiveController.value.text.characters.length;
914

915
  bool get _hasIntrinsicError => widget.maxLength != null && widget.maxLength! > 0 && _effectiveController.value.text.characters.length > widget.maxLength!;
916 917 918

  bool get _hasError => widget.decoration?.errorText != null || _hasIntrinsicError;

919 920
  Color get _errorColor => widget.decoration?.errorStyle?.color ?? Theme.of(context).colorScheme.error;

921
  InputDecoration _getEffectiveDecoration() {
922
    final MaterialLocalizations localizations = MaterialLocalizations.of(context);
923
    final ThemeData themeData = Theme.of(context);
924
    final InputDecoration effectiveDecoration = (widget.decoration ?? const InputDecoration())
925
      .applyDefaults(themeData.inputDecorationTheme)
926
      .copyWith(
927
        enabled: _isEnabled,
928
        hintMaxLines: widget.decoration?.hintMaxLines ?? widget.maxLines,
929
      );
930

931
    // No need to build anything if counter or counterText were given directly.
932
    if (effectiveDecoration.counter != null || effectiveDecoration.counterText != null) {
933
      return effectiveDecoration;
934
    }
935

936
    // If buildCounter was provided, use it to generate a counter widget.
937
    Widget? counter;
938
    final int currentLength = _currentLength;
939 940 941 942
    if (effectiveDecoration.counter == null
        && effectiveDecoration.counterText == null
        && widget.buildCounter != null) {
      final bool isFocused = _effectiveFocusNode.hasFocus;
943
      final Widget? builtCounter = widget.buildCounter!(
944 945 946 947
        context,
        currentLength: currentLength,
        maxLength: widget.maxLength,
        isFocused: isFocused,
948
      );
949 950 951 952 953 954 955 956
      // If buildCounter returns null, don't add a counter widget to the field.
      if (builtCounter != null) {
        counter = Semantics(
          container: true,
          liveRegion: isFocused,
          child: builtCounter,
        );
      }
957 958 959
      return effectiveDecoration.copyWith(counter: counter);
    }

960 961 962
    if (widget.maxLength == null) {
      return effectiveDecoration;
    } // No counter widget
963

964 965 966
    String counterText = '$currentLength';
    String semanticCounterText = '';

967
    // Handle a real maxLength (positive number)
968
    if (widget.maxLength! > 0) {
969
      // Show the maxLength in the counter
970
      counterText += '/${widget.maxLength}';
971
      final int remaining = (widget.maxLength! - currentLength).clamp(0, widget.maxLength!); // ignore_clamp_double_lint
972
      semanticCounterText = localizations.remainingTextFieldCharacterCount(remaining);
973
    }
974

975 976 977 978
    if (_hasIntrinsicError) {
      return effectiveDecoration.copyWith(
        errorText: effectiveDecoration.errorText ?? '',
        counterStyle: effectiveDecoration.errorStyle
979
          ?? (themeData.useMaterial3 ? _m3CounterErrorStyle(context): _m2CounterErrorStyle(context)),
980 981 982
        counterText: counterText,
        semanticCounterText: semanticCounterText,
      );
983
    }
984

985 986 987 988
    return effectiveDecoration.copyWith(
      counterText: counterText,
      semanticCounterText: semanticCounterText,
    );
989 990
  }

991 992 993
  @override
  void initState() {
    super.initState();
994
    _selectionGestureDetectorBuilder = _TextFieldSelectionGestureDetectorBuilder(state: this);
995
    if (widget.controller == null) {
996
      _createLocalController();
997
    }
998
    _effectiveFocusNode.canRequestFocus = widget.canRequestFocus && _isEnabled;
999
    _effectiveFocusNode.addListener(_handleFocusChanged);
1000 1001
  }

1002
  bool get _canRequestFocus {
1003
    final NavigationMode mode = MediaQuery.maybeNavigationModeOf(context) ?? NavigationMode.traditional;
1004 1005
    switch (mode) {
      case NavigationMode.traditional:
1006
        return widget.canRequestFocus && _isEnabled;
1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017
      case NavigationMode.directional:
        return true;
    }
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _effectiveFocusNode.canRequestFocus = _canRequestFocus;
  }

1018
  @override
1019
  void didUpdateWidget(TextField oldWidget) {
1020
    super.didUpdateWidget(oldWidget);
1021
    if (widget.controller == null && oldWidget.controller != null) {
1022
      _createLocalController(oldWidget.controller!.value);
1023
    } else if (widget.controller != null && oldWidget.controller == null) {
1024 1025
      unregisterFromRestoration(_controller!);
      _controller!.dispose();
1026
      _controller = null;
1027
    }
1028 1029 1030 1031 1032 1033

    if (widget.focusNode != oldWidget.focusNode) {
      (oldWidget.focusNode ?? _focusNode)?.removeListener(_handleFocusChanged);
      (widget.focusNode ?? _focusNode)?.addListener(_handleFocusChanged);
    }

1034
    _effectiveFocusNode.canRequestFocus = _canRequestFocus;
1035

1036
    if (_effectiveFocusNode.hasFocus && widget.readOnly != oldWidget.readOnly && _isEnabled) {
1037 1038 1039 1040
      if(_effectiveController.selection.isCollapsed) {
        _showSelectionHandles = !widget.readOnly;
      }
    }
1041 1042
  }

1043
  @override
1044
  void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
1045 1046 1047 1048 1049 1050 1051
    if (_controller != null) {
      _registerController();
    }
  }

  void _registerController() {
    assert(_controller != null);
1052
    registerForRestoration(_controller!, 'controller');
1053 1054
  }

1055
  void _createLocalController([TextEditingValue? value]) {
1056 1057 1058 1059 1060 1061 1062 1063 1064 1065
    assert(_controller == null);
    _controller = value == null
        ? RestorableTextEditingController()
        : RestorableTextEditingController.fromValue(value);
    if (!restorePending) {
      _registerController();
    }
  }

  @override
1066
  String? get restorationId => widget.restorationId;
1067

1068 1069
  @override
  void dispose() {
1070
    _effectiveFocusNode.removeListener(_handleFocusChanged);
1071
    _focusNode?.dispose();
1072
    _controller?.dispose();
1073 1074 1075
    super.dispose();
  }

1076
  EditableTextState? get _editableText => editableTextKey.currentState;
1077

1078
  void _requestKeyboard() {
1079 1080 1081
    _editableText?.requestKeyboard();
  }

1082
  bool _shouldShowSelectionHandles(SelectionChangedCause? cause) {
1083 1084
    // When the text field is activated by something that doesn't trigger the
    // selection overlay, we shouldn't show the handles either.
1085
    if (!_selectionGestureDetectorBuilder.shouldShowSelectionToolbar) {
1086
      return false;
1087
    }
1088

1089
    if (cause == SelectionChangedCause.keyboard) {
1090
      return false;
1091
    }
1092

1093
    if (widget.readOnly && _effectiveController.selection.isCollapsed) {
1094
      return false;
1095
    }
1096

1097
    if (!_isEnabled) {
1098
      return false;
1099
    }
1100

1101
    if (cause == SelectionChangedCause.longPress || cause == SelectionChangedCause.scribble) {
1102
      return true;
1103
    }
1104

1105
    if (_effectiveController.text.isNotEmpty) {
1106
      return true;
1107
    }
1108 1109

    return false;
1110 1111
  }

1112 1113 1114 1115 1116 1117 1118
  void _handleFocusChanged() {
    setState(() {
      // Rebuild the widget on focus change to show/hide the text selection
      // highlight.
    });
  }

1119
  void _handleSelectionChanged(TextSelection selection, SelectionChangedCause? cause) {
1120 1121 1122 1123 1124
    final bool willShowSelectionHandles = _shouldShowSelectionHandles(cause);
    if (willShowSelectionHandles != _showSelectionHandles) {
      setState(() {
        _showSelectionHandles = willShowSelectionHandles;
      });
1125 1126
    }

1127
    switch (Theme.of(context).platform) {
1128
      case TargetPlatform.iOS:
1129
      case TargetPlatform.macOS:
1130 1131
      case TargetPlatform.linux:
      case TargetPlatform.windows:
1132 1133
      case TargetPlatform.fuchsia:
      case TargetPlatform.android:
1134
        if (cause == SelectionChangedCause.longPress) {
1135 1136
          _editableText?.bringIntoView(selection.extent);
        }
1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149
    }

    switch (Theme.of(context).platform) {
      case TargetPlatform.iOS:
      case TargetPlatform.fuchsia:
      case TargetPlatform.android:
        break;
      case TargetPlatform.macOS:
      case TargetPlatform.linux:
      case TargetPlatform.windows:
        if (cause == SelectionChangedCause.drag) {
          _editableText?.hideToolbar();
        }
1150 1151 1152
    }
  }

1153 1154 1155
  /// Toggle the toolbar when a selection handle is tapped.
  void _handleSelectionHandleTapped() {
    if (_effectiveController.selection.isCollapsed) {
1156
      _editableText!.toggleToolbar();
1157 1158 1159
    }
  }

1160 1161 1162
  void _handleHover(bool hovering) {
    if (hovering != _isHovering) {
      setState(() {
1163
        _isHovering = hovering;
1164 1165 1166 1167
      });
    }
  }

1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190
  // AutofillClient implementation start.
  @override
  String get autofillId => _editableText!.autofillId;

  @override
  void autofill(TextEditingValue newEditingValue) => _editableText!.autofill(newEditingValue);

  @override
  TextInputConfiguration get textInputConfiguration {
    final List<String>? autofillHints = widget.autofillHints?.toList(growable: false);
    final AutofillConfiguration autofillConfiguration = autofillHints != null
      ? AutofillConfiguration(
          uniqueIdentifier: autofillId,
          autofillHints: autofillHints,
          currentEditingValue: _effectiveController.value,
          hintText: (widget.decoration ?? const InputDecoration()).hintText,
        )
      : AutofillConfiguration.disabled;

    return _editableText!.textInputConfiguration.copyWith(autofillConfiguration: autofillConfiguration);
  }
  // AutofillClient implementation end.

1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206
  Set<MaterialState> get _materialState {
    return <MaterialState>{
      if (!_isEnabled) MaterialState.disabled,
      if (_isHovering) MaterialState.hovered,
      if (_effectiveFocusNode.hasFocus) MaterialState.focused,
      if (_hasError) MaterialState.error,
    };
  }

  TextStyle _getInputStyleForState(TextStyle style) {
    final ThemeData theme = Theme.of(context);
    final TextStyle stateStyle = MaterialStateProperty.resolveAs(theme.useMaterial3 ? _m3StateInputStyle(context)! : _m2StateInputStyle(context)!, _materialState);
    final TextStyle providedStyle = MaterialStateProperty.resolveAs(style, _materialState);
    return providedStyle.merge(stateStyle);
  }

1207 1208
  @override
  Widget build(BuildContext context) {
1209
    assert(debugCheckHasMaterial(context));
1210
    assert(debugCheckHasMaterialLocalizations(context));
1211
    assert(debugCheckHasDirectionality(context));
1212
    assert(
1213 1214
      !(widget.style != null && widget.style!.inherit == false &&
        (widget.style!.fontSize == null || widget.style!.textBaseline == null)),
1215 1216 1217
      'inherit false style must supply fontSize and textBaseline',
    );

1218
    final ThemeData theme = Theme.of(context);
1219
    final DefaultSelectionStyle selectionStyle = DefaultSelectionStyle.of(context);
1220
    final TextStyle style = _getInputStyleForState(theme.useMaterial3 ? _m3InputStyle(context) : theme.textTheme.titleMedium!).merge(widget.style);
1221
    final Brightness keyboardAppearance = widget.keyboardAppearance ?? theme.brightness;
1222 1223
    final TextEditingController controller = _effectiveController;
    final FocusNode focusNode = _effectiveFocusNode;
1224 1225
    final List<TextInputFormatter> formatters = <TextInputFormatter>[
      ...?widget.inputFormatters,
1226
      if (widget.maxLength != null)
1227 1228 1229 1230
        LengthLimitingTextInputFormatter(
          widget.maxLength,
          maxLengthEnforcement: _effectiveMaxLengthEnforcement,
        ),
1231
    ];
1232

1233 1234 1235 1236 1237 1238 1239 1240
    // Set configuration as disabled if not otherwise specified. If specified,
    // ensure that configuration uses Material text style for misspelled words
    // unless a custom style is specified.
    final SpellCheckConfiguration spellCheckConfiguration =
      widget.spellCheckConfiguration != null &&
      widget.spellCheckConfiguration != const SpellCheckConfiguration.disabled()
        ? widget.spellCheckConfiguration!.copyWith(
            misspelledTextStyle: widget.spellCheckConfiguration!.misspelledTextStyle
1241 1242 1243
              ?? TextField.materialMisspelledTextStyle,
            spellCheckSuggestionsToolbarBuilder:
              widget.spellCheckConfiguration!.spellCheckSuggestionsToolbarBuilder
1244
                ?? TextField.defaultSpellCheckSuggestionsToolbarBuilder,
1245
          )
1246 1247
        : const SpellCheckConfiguration.disabled();

1248
    TextSelectionControls? textSelectionControls = widget.selectionControls;
1249
    final bool paintCursorAboveText;
1250
    bool? cursorOpacityAnimates = widget.cursorOpacityAnimates;
1251
    Offset? cursorOffset;
1252
    final Color cursorColor;
1253
    final Color selectionColor;
1254 1255
    Color? autocorrectionTextRectColor;
    Radius? cursorRadius = widget.cursorRadius;
1256
    VoidCallback? handleDidGainAccessibilityFocus;
1257

1258
    switch (theme.platform) {
1259
      case TargetPlatform.iOS:
1260
        final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context);
1261
        forcePressEnabled = true;
1262
        textSelectionControls ??= cupertinoTextSelectionHandleControls;
1263
        paintCursorAboveText = true;
1264
        cursorOpacityAnimates ??= true;
1265
        cursorColor = _hasError ? _errorColor : widget.cursorColor ?? selectionStyle.cursorColor ?? cupertinoTheme.primaryColor;
1266
        selectionColor = selectionStyle.selectionColor ?? cupertinoTheme.primaryColor.withOpacity(0.40);
1267
        cursorRadius ??= const Radius.circular(2.0);
1268
        cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.devicePixelRatioOf(context), 0);
1269
        autocorrectionTextRectColor = selectionColor;
1270

1271 1272 1273
      case TargetPlatform.macOS:
        final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context);
        forcePressEnabled = false;
1274
        textSelectionControls ??= cupertinoDesktopTextSelectionHandleControls;
1275
        paintCursorAboveText = true;
1276
        cursorOpacityAnimates ??= false;
1277
        cursorColor = _hasError ? _errorColor : widget.cursorColor ?? selectionStyle.cursorColor ?? cupertinoTheme.primaryColor;
1278
        selectionColor = selectionStyle.selectionColor ?? cupertinoTheme.primaryColor.withOpacity(0.40);
1279
        cursorRadius ??= const Radius.circular(2.0);
1280
        cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.devicePixelRatioOf(context), 0);
1281
        handleDidGainAccessibilityFocus = () {
1282
          // Automatically activate the TextField when it receives accessibility focus.
1283 1284 1285 1286
          if (!_effectiveFocusNode.hasFocus && _effectiveFocusNode.canRequestFocus) {
            _effectiveFocusNode.requestFocus();
          }
        };
1287

1288 1289
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
1290
        forcePressEnabled = false;
1291
        textSelectionControls ??= materialTextSelectionHandleControls;
1292
        paintCursorAboveText = false;
1293
        cursorOpacityAnimates ??= false;
1294
        cursorColor = _hasError ? _errorColor : widget.cursorColor ?? selectionStyle.cursorColor ?? theme.colorScheme.primary;
1295
        selectionColor = selectionStyle.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40);
1296

1297
      case TargetPlatform.linux:
1298
        forcePressEnabled = false;
1299
        textSelectionControls ??= desktopTextSelectionHandleControls;
1300
        paintCursorAboveText = false;
1301
        cursorOpacityAnimates ??= false;
1302
        cursorColor = _hasError ? _errorColor : widget.cursorColor ?? selectionStyle.cursorColor ?? theme.colorScheme.primary;
1303
        selectionColor = selectionStyle.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40);
1304

1305
      case TargetPlatform.windows:
1306
        forcePressEnabled = false;
1307
        textSelectionControls ??= desktopTextSelectionHandleControls;
1308
        paintCursorAboveText = false;
1309
        cursorOpacityAnimates ??= false;
1310
        cursorColor = _hasError ? _errorColor : widget.cursorColor ?? selectionStyle.cursorColor ?? theme.colorScheme.primary;
1311
        selectionColor = selectionStyle.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40);
1312 1313 1314 1315 1316 1317
        handleDidGainAccessibilityFocus = () {
          // Automatically activate the TextField when it receives accessibility focus.
          if (!_effectiveFocusNode.hasFocus && _effectiveFocusNode.canRequestFocus) {
            _effectiveFocusNode.requestFocus();
          }
        };
1318 1319
    }

1320
    Widget child = RepaintBoundary(
1321 1322 1323 1324 1325 1326 1327 1328 1329 1330
      child: UnmanagedRestorationScope(
        bucket: bucket,
        child: EditableText(
          key: editableTextKey,
          readOnly: widget.readOnly || !_isEnabled,
          toolbarOptions: widget.toolbarOptions,
          showCursor: widget.showCursor,
          showSelectionHandles: _showSelectionHandles,
          controller: controller,
          focusNode: focusNode,
1331
          undoController: widget.undoController,
1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348
          keyboardType: widget.keyboardType,
          textInputAction: widget.textInputAction,
          textCapitalization: widget.textCapitalization,
          style: style,
          strutStyle: widget.strutStyle,
          textAlign: widget.textAlign,
          textDirection: widget.textDirection,
          autofocus: widget.autofocus,
          obscuringCharacter: widget.obscuringCharacter,
          obscureText: widget.obscureText,
          autocorrect: widget.autocorrect,
          smartDashesType: widget.smartDashesType,
          smartQuotesType: widget.smartQuotesType,
          enableSuggestions: widget.enableSuggestions,
          maxLines: widget.maxLines,
          minLines: widget.minLines,
          expands: widget.expands,
1349 1350
          // Only show the selection highlight when the text field is focused.
          selectionColor: focusNode.hasFocus ? selectionColor : null,
1351 1352 1353 1354 1355 1356 1357
          selectionControls: widget.selectionEnabled ? textSelectionControls : null,
          onChanged: widget.onChanged,
          onSelectionChanged: _handleSelectionChanged,
          onEditingComplete: widget.onEditingComplete,
          onSubmitted: widget.onSubmitted,
          onAppPrivateCommand: widget.onAppPrivateCommand,
          onSelectionHandleTapped: _handleSelectionHandleTapped,
1358
          onTapOutside: widget.onTapOutside,
1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377
          inputFormatters: formatters,
          rendererIgnoresPointer: true,
          mouseCursor: MouseCursor.defer, // TextField will handle the cursor
          cursorWidth: widget.cursorWidth,
          cursorHeight: widget.cursorHeight,
          cursorRadius: cursorRadius,
          cursorColor: cursorColor,
          selectionHeightStyle: widget.selectionHeightStyle,
          selectionWidthStyle: widget.selectionWidthStyle,
          cursorOpacityAnimates: cursorOpacityAnimates,
          cursorOffset: cursorOffset,
          paintCursorAboveText: paintCursorAboveText,
          backgroundCursorColor: CupertinoColors.inactiveGray,
          scrollPadding: widget.scrollPadding,
          keyboardAppearance: keyboardAppearance,
          enableInteractiveSelection: widget.enableInteractiveSelection,
          dragStartBehavior: widget.dragStartBehavior,
          scrollController: widget.scrollController,
          scrollPhysics: widget.scrollPhysics,
1378
          autofillClient: this,
1379
          autocorrectionTextRectColor: autocorrectionTextRectColor,
1380
          clipBehavior: widget.clipBehavior,
1381
          restorationId: 'editable',
1382
          scribbleEnabled: widget.scribbleEnabled,
1383
          enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,
1384
          contentInsertionConfiguration: widget.contentInsertionConfiguration,
1385
          contextMenuBuilder: widget.contextMenuBuilder,
1386
          spellCheckConfiguration: spellCheckConfiguration,
1387
          magnifierConfiguration: widget.magnifierConfiguration ?? TextMagnifier.adaptiveMagnifierConfiguration,
1388
        ),
1389 1390 1391
      ),
    );

1392
    if (widget.decoration != null) {
1393 1394
      child = AnimatedBuilder(
        animation: Listenable.merge(<Listenable>[ focusNode, controller ]),
1395
        builder: (BuildContext context, Widget? child) {
1396
          return InputDecorator(
1397
            decoration: _getEffectiveDecoration(),
1398 1399
            baseStyle: widget.style,
            textAlign: widget.textAlign,
1400
            textAlignVertical: widget.textAlignVertical,
1401
            isHovering: _isHovering,
1402 1403
            isFocused: focusNode.hasFocus,
            isEmpty: controller.value.text.isEmpty,
1404
            expands: widget.expands,
1405 1406 1407 1408 1409 1410
            child: child,
          );
        },
        child: child,
      );
    }
1411 1412
    final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor>(
      widget.mouseCursor ?? MaterialStateMouseCursor.textable,
1413
      _materialState,
1414 1415
    );

1416
    final int? semanticsMaxValueLength;
1417
    if (_effectiveMaxLengthEnforcement != MaxLengthEnforcement.none &&
1418 1419 1420 1421 1422 1423 1424
      widget.maxLength != null &&
      widget.maxLength! > 0) {
      semanticsMaxValueLength = widget.maxLength;
    } else {
      semanticsMaxValueLength = null;
    }

1425 1426 1427 1428 1429
    return MouseRegion(
      cursor: effectiveMouseCursor,
      onEnter: (PointerEnterEvent event) => _handleHover(true),
      onExit: (PointerExitEvent event) => _handleHover(false),
      child: TextFieldTapRegion(
1430 1431 1432 1433 1434 1435 1436 1437 1438
        child: IgnorePointer(
          ignoring: !_isEnabled,
          child: AnimatedBuilder(
            animation: controller, // changes the _currentLength
            builder: (BuildContext context, Widget? child) {
              return Semantics(
                maxValueLength: semanticsMaxValueLength,
                currentValueLength: _currentLength,
                onTap: widget.readOnly ? null : () {
1439
                  if (!_effectiveController.selection.isValid) {
1440
                    _effectiveController.selection = TextSelection.collapsed(offset: _effectiveController.text.length);
1441
                  }
1442 1443 1444 1445 1446 1447 1448 1449
                  _requestKeyboard();
                },
                onDidGainAccessibilityFocus: handleDidGainAccessibilityFocus,
                child: child,
              );
            },
            child: _selectionGestureDetectorBuilder.buildGestureDetector(
              behavior: HitTestBehavior.translucent,
1450
              child: child,
1451
            ),
1452
          ),
1453
        ),
1454
      ),
1455 1456
    );
  }
1457
}
1458

1459 1460 1461 1462 1463 1464 1465 1466
TextStyle? _m2StateInputStyle(BuildContext context) => MaterialStateTextStyle.resolveWith((Set<MaterialState> states) {
  final ThemeData theme = Theme.of(context);
  if (states.contains(MaterialState.disabled)) {
    return TextStyle(color: theme.disabledColor);
  }
  return TextStyle(color: theme.textTheme.titleMedium?.color);
});

1467
TextStyle _m2CounterErrorStyle(BuildContext context) =>
1468
  Theme.of(context).textTheme.bodySmall!.copyWith(color: Theme.of(context).colorScheme.error);
1469

1470 1471 1472 1473 1474 1475 1476
// BEGIN GENERATED TOKEN PROPERTIES - TextField

// Do not edit by hand. The code between the "BEGIN GENERATED" and
// "END GENERATED" comments are generated from data in the Material
// Design token database by the script:
//   dev/tools/gen_defaults/bin/gen_defaults.dart.

1477
// Token database version: v0_162
1478

1479 1480 1481 1482 1483 1484 1485
TextStyle? _m3StateInputStyle(BuildContext context) => MaterialStateTextStyle.resolveWith((Set<MaterialState> states) {
  if (states.contains(MaterialState.disabled)) {
    return TextStyle(color: Theme.of(context).textTheme.bodyLarge!.color?.withOpacity(0.38));
  }
  return TextStyle(color: Theme.of(context).textTheme.bodyLarge!.color);
});

1486 1487
TextStyle _m3InputStyle(BuildContext context) => Theme.of(context).textTheme.bodyLarge!;

1488
TextStyle _m3CounterErrorStyle(BuildContext context) =>
1489
  Theme.of(context).textTheme.bodySmall!.copyWith(color: Theme.of(context).colorScheme.error);
1490

1491
// END GENERATED TOKEN PROPERTIES - TextField