text_field.dart 48.4 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;

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

15
import 'debug.dart';
16
import 'feedback.dart';
17
import 'input_decorator.dart';
18
import 'material.dart';
19
import 'material_localizations.dart';
20
import 'material_state.dart';
21
import 'selectable_text.dart' show iOSHorizontalOffset;
22
import 'text_selection.dart';
23
import 'text_selection_theme.dart';
24 25
import 'theme.dart';

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

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

41 42
class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDetectorBuilder {
  _TextFieldSelectionGestureDetectorBuilder({
43
    required _TextFieldState state,
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
  }) : _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
  void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) {
    if (delegate.selectionEnabled) {
65
      switch (Theme.of(_state.context)!.platform) {
66
        case TargetPlatform.iOS:
67
        case TargetPlatform.macOS:
68 69 70 71 72 73 74
          renderEditable.selectPositionAt(
            from: details.globalPosition,
            cause: SelectionChangedCause.longPress,
          );
          break;
        case TargetPlatform.android:
        case TargetPlatform.fuchsia:
75 76
        case TargetPlatform.linux:
        case TargetPlatform.windows:
77 78 79 80 81 82 83 84 85 86 87 88 89 90
          renderEditable.selectWordsInRange(
            from: details.globalPosition - details.offsetFromOrigin,
            to: details.globalPosition,
            cause: SelectionChangedCause.longPress,
          );
          break;
      }
    }
  }

  @override
  void onSingleTapUp(TapUpDetails details) {
    editableText.hideToolbar();
    if (delegate.selectionEnabled) {
91
      switch (Theme.of(_state.context)!.platform) {
92
        case TargetPlatform.iOS:
93
        case TargetPlatform.macOS:
94 95 96 97 98 99 100 101 102 103 104 105 106 107
          switch (details.kind) {
            case PointerDeviceKind.mouse:
            case PointerDeviceKind.stylus:
            case PointerDeviceKind.invertedStylus:
              // Precise devices should place the cursor at a precise position.
              renderEditable.selectPosition(cause: SelectionChangedCause.tap);
              break;
            case PointerDeviceKind.touch:
            case PointerDeviceKind.unknown:
              // On macOS/iOS/iPadOS a touch tap places the cursor at the edge
              // of the word.
              renderEditable.selectWordEdge(cause: SelectionChangedCause.tap);
              break;
          }
108 109 110
          break;
        case TargetPlatform.android:
        case TargetPlatform.fuchsia:
111 112
        case TargetPlatform.linux:
        case TargetPlatform.windows:
113 114 115 116 117 118
          renderEditable.selectPosition(cause: SelectionChangedCause.tap);
          break;
      }
    }
    _state._requestKeyboard();
    if (_state.widget.onTap != null)
119
      _state.widget.onTap!();
120 121 122 123 124
  }

  @override
  void onSingleLongTapStart(LongPressStartDetails details) {
    if (delegate.selectionEnabled) {
125
      switch (Theme.of(_state.context)!.platform) {
126
        case TargetPlatform.iOS:
127
        case TargetPlatform.macOS:
128 129 130 131 132 133 134
          renderEditable.selectPositionAt(
            from: details.globalPosition,
            cause: SelectionChangedCause.longPress,
          );
          break;
        case TargetPlatform.android:
        case TargetPlatform.fuchsia:
135 136
        case TargetPlatform.linux:
        case TargetPlatform.windows:
137
          renderEditable.selectWord(cause: SelectionChangedCause.longPress);
138 139 140 141 142 143 144
          Feedback.forLongPress(_state.context);
          break;
      }
    }
  }
}

145
/// A material design text field.
146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167
///
/// 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
168
/// one of its ancestors to be a [Material] widget.
169 170 171 172
///
/// To integrate the [TextField] into a [Form] with other [FormField] widgets,
/// consider using [TextFormField].
///
173 174 175
/// 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.
176
///
177
/// {@tool snippet}
178 179 180 181
/// 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.
///
182
/// ![](https://flutter.github.io/assets-for-api-docs/assets/material/text_field.png)
183
///
184 185 186 187 188 189 190 191 192 193 194
/// ```dart
/// TextField(
///   obscureText: true,
///   decoration: InputDecoration(
///     border: OutlineInputBorder(),
///     labelText: 'Password',
///   ),
/// )
/// ```
/// {@end-tool}
///
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229
/// ## 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.
///
/// {@tool dartpad --template=stateful_widget_material}
///
/// This sample shows how to get a value from a TextField via the [onSubmitted]
/// callback.
///
/// ```dart
/// TextEditingController _controller;
///
/// void initState() {
///   super.initState();
///   _controller = TextEditingController();
/// }
///
/// void dispose() {
///   _controller.dispose();
///   super.dispose();
/// }
///
/// Widget build(BuildContext context) {
///   return Scaffold(
///     body: Center(
///       child: TextField(
///         controller: _controller,
///         onSubmitted: (String value) async {
///           await showDialog<void>(
///             context: context,
///             builder: (BuildContext context) {
///               return AlertDialog(
///                 title: const Text('Thanks!'),
230
///                 content: Text ('You typed "$value", which has length ${value.characters.length}.'),
231
///                 actions: <Widget>[
232
///                   TextButton(
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258
///                     onPressed: () { Navigator.pop(context); },
///                     child: const Text('OK'),
///                   ),
///                 ],
///               );
///             },
///           );
///         },
///       ),
///     ),
///   );
/// }
/// ```
/// {@end-tool}
///
/// 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].
259 260 261 262 263 264 265 266
///
  /// ## Handling emojis and other complex characters
/// {@macro flutter.widgets.editableText.complexCharacters}
///
/// 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.
267
///
268 269 270 271 272 273
/// 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
274
///    [TextField]. The [EditableText] widget is rarely used directly unless
275
///    you are implementing an entirely different design language, such as
276
///    Cupertino.
277 278 279 280 281
///  * <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)
282 283 284 285 286 287 288 289 290
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.
291 292
  ///
  /// The [maxLines] property can be set to null to remove the restriction on
293
  /// the number of lines. By default, it is one, meaning this is a single-line
294
  /// text field. [maxLines] must not be zero.
295 296 297
  ///
  /// The [maxLength] property is set to null by default, which means the
  /// number of characters allowed in the text field is not restricted. If
298 299 300 301 302
  /// [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
  /// number of characters to be entered.  If the value is set to
  /// [TextField.noMaxLength] then only the current length is displayed.
303 304
  ///
  /// After [maxLength] characters have been input, additional input
305
  /// is ignored, unless [maxLengthEnforced] is set to false. The text field
306 307 308 309 310 311 312
  /// 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.
  ///
  /// If [maxLengthEnforced] is set to false, 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.
313
  ///
314 315 316
  /// The text cursor is not shown if [showCursor] is false or if [showCursor]
  /// is null (the default) and [readOnly] is true.
  ///
317 318 319 320 321
  /// 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.
  ///
322
  /// The [textAlign], [autofocus], [obscureText], [readOnly], [autocorrect],
323
  /// [maxLengthEnforced], [scrollPadding], [maxLines], [maxLength],
324 325
  /// [selectionHeightStyle], [selectionWidthStyle], and [enableSuggestions]
  /// arguments must not be null.
326 327 328 329 330
  ///
  /// See also:
  ///
  ///  * [maxLength], which discusses the precise meaning of "number of
  ///    characters" and how it may differ from the intuitive meaning.
331
  const TextField({
332
    Key? key,
333 334
    this.controller,
    this.focusNode,
335
    this.decoration = const InputDecoration(),
336
    TextInputType? keyboardType,
337
    this.textInputAction,
338
    this.textCapitalization = TextCapitalization.none,
339
    this.style,
340
    this.strutStyle,
341
    this.textAlign = TextAlign.start,
342
    this.textAlignVertical,
343
    this.textDirection,
344
    this.readOnly = false,
345
    ToolbarOptions? toolbarOptions,
346
    this.showCursor,
347
    this.autofocus = false,
348
    this.obscuringCharacter = '•',
349 350
    this.obscureText = false,
    this.autocorrect = true,
351 352
    SmartDashesType? smartDashesType,
    SmartQuotesType? smartQuotesType,
353
    this.enableSuggestions = true,
354
    this.maxLines = 1,
355 356
    this.minLines,
    this.expands = false,
357
    this.maxLength,
358
    this.maxLengthEnforced = true,
359
    this.onChanged,
360
    this.onEditingComplete,
361
    this.onSubmitted,
362
    this.onAppPrivateCommand,
363
    this.inputFormatters,
364
    this.enabled,
365
    this.cursorWidth = 2.0,
366
    this.cursorHeight,
367 368
    this.cursorRadius,
    this.cursorColor,
369 370
    this.selectionHeightStyle = ui.BoxHeightStyle.tight,
    this.selectionWidthStyle = ui.BoxWidthStyle.tight,
371
    this.keyboardAppearance,
372
    this.scrollPadding = const EdgeInsets.all(20.0),
373
    this.dragStartBehavior = DragStartBehavior.start,
374
    this.enableInteractiveSelection = true,
375
    this.onTap,
376
    this.mouseCursor,
377
    this.buildCounter,
378
    this.scrollController,
379
    this.scrollPhysics,
380
    this.autofillHints,
381
    this.restorationId,
382
  }) : assert(textAlign != null),
383
       assert(readOnly != null),
384
       assert(autofocus != null),
385
       assert(obscuringCharacter != null && obscuringCharacter.length == 1),
386
       assert(obscureText != null),
387
       assert(autocorrect != null),
388 389
       smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
       smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
390
       assert(enableSuggestions != null),
391
       assert(enableInteractiveSelection != null),
392
       assert(maxLengthEnforced != null),
393
       assert(scrollPadding != null),
394
       assert(dragStartBehavior != null),
395 396
       assert(selectionHeightStyle != null),
       assert(selectionWidthStyle != null),
397
       assert(maxLines == null || maxLines > 0),
398 399 400
       assert(minLines == null || minLines > 0),
       assert(
         (maxLines == null) || (minLines == null) || (maxLines >= minLines),
401
         "minLines can't be greater than maxLines",
402 403 404 405 406 407
       ),
       assert(expands != null),
       assert(
         !expands || (maxLines == null && minLines == null),
         'minLines and maxLines must be null when expands is true.',
       ),
408
       assert(!obscureText || maxLines == 1, 'Obscured fields cannot be multiline.'),
409
       assert(maxLength == null || maxLength == TextField.noMaxLength || maxLength > 0),
410 411 412 413 414
       // Assert the following instead of setting it directly to avoid surprising the user by silently changing the value they set.
       assert(!identical(textInputAction, TextInputAction.newline) ||
         maxLines == 1 ||
         !identical(keyboardType, TextInputType.text),
         'Use keyboardType TextInputType.multiline when using TextInputAction.newline on a multiline TextField.'),
415
       keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
416
       toolbarOptions = toolbarOptions ?? (obscureText ?
417 418 419 420 421 422 423 424 425
         const ToolbarOptions(
           selectAll: true,
           paste: true,
         ) :
         const ToolbarOptions(
           copy: true,
           cut: true,
           selectAll: true,
           paste: true,
426
         )),
427
       super(key: key);
428 429 430

  /// Controls the text being edited.
  ///
431
  /// If null, this widget will create its own [TextEditingController].
432
  final TextEditingController? controller;
433

434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453
  /// 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
  /// focusNode.addListener(() { print(myFocusNode.hasFocus); });
  /// ```
454 455
  ///
  /// If null, this widget will create its own [FocusNode].
456 457 458
  ///
  /// ## Keyboard
  ///
Gary Qian's avatar
Gary Qian committed
459
  /// Requesting the focus will typically cause the keyboard to be shown
460 461
  /// if it's not showing already.
  ///
462
  /// On Android, the user can hide the keyboard - without changing the focus -
463 464 465 466 467 468 469 470 471
  /// with the system back button. They can restore the keyboard's visibility
  /// by tapping on a text field.  The user might hide the keyboard and
  /// 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()].
472
  final FocusNode? focusNode;
473 474 475

  /// The decoration to show around the text field.
  ///
476
  /// By default, draws a horizontal line under the text field but can be
477 478
  /// configured to show an icon, label, hint text, and error text.
  ///
479
  /// Specify null to remove the decoration entirely (including the
480
  /// extra padding introduced by the decoration to save space for the labels).
481
  final InputDecoration? decoration;
482

483
  /// {@macro flutter.widgets.editableText.keyboardType}
484 485
  final TextInputType keyboardType;

486 487
  /// The type of action button to use for the keyboard.
  ///
488 489
  /// Defaults to [TextInputAction.newline] if [keyboardType] is
  /// [TextInputType.multiline] and [TextInputAction.done] otherwise.
490
  final TextInputAction? textInputAction;
491

492
  /// {@macro flutter.widgets.editableText.textCapitalization}
493 494
  final TextCapitalization textCapitalization;

495 496 497 498
  /// The style to use for the text being edited.
  ///
  /// This text style is also used as the base style for the [decoration].
  ///
499
  /// If null, defaults to the `subtitle1` text style from the current [Theme].
500
  final TextStyle? style;
501

502
  /// {@macro flutter.widgets.editableText.strutStyle}
503
  final StrutStyle? strutStyle;
504

505
  /// {@macro flutter.widgets.editableText.textAlign}
506 507
  final TextAlign textAlign;

Dan Field's avatar
Dan Field committed
508
  /// {@macro flutter.widgets.inputDecorator.textAlignVertical}
509
  final TextAlignVertical? textAlignVertical;
510

511
  /// {@macro flutter.widgets.editableText.textDirection}
512
  final TextDirection? textDirection;
513

514
  /// {@macro flutter.widgets.editableText.autofocus}
515 516
  final bool autofocus;

517 518 519
  /// {@macro flutter.widgets.editableText.obscuringCharacter}
  final String obscuringCharacter;

520
  /// {@macro flutter.widgets.editableText.obscureText}
521 522
  final bool obscureText;

523
  /// {@macro flutter.widgets.editableText.autocorrect}
524 525
  final bool autocorrect;

526 527 528 529 530 531
  /// {@macro flutter.services.textInput.smartDashesType}
  final SmartDashesType smartDashesType;

  /// {@macro flutter.services.textInput.smartQuotesType}
  final SmartQuotesType smartQuotesType;

532 533 534
  /// {@macro flutter.services.textInput.enableSuggestions}
  final bool enableSuggestions;

535
  /// {@macro flutter.widgets.editableText.maxLines}
536
  final int? maxLines;
537

538
  /// {@macro flutter.widgets.editableText.minLines}
539
  final int? minLines;
540 541 542 543

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

544 545 546
  /// {@macro flutter.widgets.editableText.readOnly}
  final bool readOnly;

547 548 549 550 551 552 553
  /// 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.
  final ToolbarOptions toolbarOptions;

554
  /// {@macro flutter.widgets.editableText.showCursor}
555
  final bool? showCursor;
556

557 558
  /// If [maxLength] is set to this value, only the "current input length"
  /// part of the character counter is shown.
559
  static const int noMaxLength = -1;
560

561 562 563 564
  /// The maximum number of characters (Unicode scalar values) to allow in the
  /// text field.
  ///
  /// If set, a character counter will be displayed below the
565
  /// field showing how many characters have been entered. If set to a number
566
  /// greater than 0, it will also display the maximum number allowed. If set
567 568 569
  /// to [TextField.noMaxLength] then only the current character count is displayed.
  ///
  /// After [maxLength] characters have been input, additional input
570
  /// is ignored, unless [maxLengthEnforced] is set to false. The text field
571 572 573
  /// enforces the length with a [LengthLimitingTextInputFormatter], which is
  /// evaluated after the supplied [inputFormatters], if any.
  ///
574 575 576 577
  /// 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.
578 579 580 581 582
  ///
  /// Whitespace characters (e.g. newline, space, tab) are included in the
  /// character count.
  ///
  /// If [maxLengthEnforced] is set to false, then more than [maxLength]
583 584 585
  /// characters may be entered, but the error counter and divider will switch
  /// to the [decoration]'s [InputDecoration.errorStyle] when the limit is
  /// exceeded.
586
  ///
587
  /// {@macro flutter.services.lengthLimitingTextInputFormatter.maxLength}
588
  final int? maxLength;
589 590 591 592 593 594 595 596 597

  /// If true, prevents the field from allowing more than [maxLength]
  /// characters.
  ///
  /// If [maxLength] is set, [maxLengthEnforced] indicates whether or not to
  /// enforce the limit, or merely provide a character counter and warning when
  /// [maxLength] is exceeded.
  final bool maxLengthEnforced;

598
  /// {@macro flutter.widgets.editableText.onChanged}
599 600 601 602 603
  ///
  /// See also:
  ///
  ///  * [inputFormatters], which are called before [onChanged]
  ///    runs and can validate and change ("format") the input value.
604
  ///  * [onEditingComplete], [onSubmitted]:
605
  ///    which are more specialized input change notifications.
606
  final ValueChanged<String>? onChanged;
607

608
  /// {@macro flutter.widgets.editableText.onEditingComplete}
609
  final VoidCallback? onEditingComplete;
610

611
  /// {@macro flutter.widgets.editableText.onSubmitted}
612 613 614 615 616 617
  ///
  /// See also:
  ///
  ///  * [EditableText.onSubmitted] for an example of how to handle moving to
  ///    the next/previous field when using [TextInputAction.next] and
  ///    [TextInputAction.previous] for [textInputAction].
618
  final ValueChanged<String>? onSubmitted;
619

620
  /// {@macro flutter.widgets.editableText.onAppPrivateCommand}
621
  final AppPrivateCommandCallback? onAppPrivateCommand;
622

623
  /// {@macro flutter.widgets.editableText.inputFormatters}
624
  final List<TextInputFormatter>? inputFormatters;
625

626
  /// If false the text field is "disabled": it ignores taps and its
627 628 629
  /// [decoration] is rendered in grey.
  ///
  /// If non-null this property overrides the [decoration]'s
630
  /// [InputDecoration.enabled] property.
631
  final bool? enabled;
632

633
  /// {@macro flutter.widgets.editableText.cursorWidth}
634 635
  final double cursorWidth;

636
  /// {@macro flutter.widgets.editableText.cursorHeight}
637
  final double? cursorHeight;
638

639
  /// {@macro flutter.widgets.editableText.cursorRadius}
640
  final Radius? cursorRadius;
641

642
  /// The color of the cursor.
643
  ///
644 645 646
  /// The cursor indicates the current location of text insertion point in
  /// the field.
  ///
647 648 649 650 651
  /// If this is null it will default to the ambient
  /// [TextSelectionThemeData.cursorColor]. If that is null, and the
  /// [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].
652
  final Color? cursorColor;
653

654 655 656 657 658 659 660 661 662 663
  /// 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;

664
  /// The appearance of the keyboard.
665
  ///
666
  /// This setting is only honored on iOS devices.
667
  ///
668
  /// If unset, defaults to the brightness of [ThemeData.primaryColorBrightness].
669
  final Brightness? keyboardAppearance;
670

671
  /// {@macro flutter.widgets.editableText.scrollPadding}
672 673
  final EdgeInsets scrollPadding;

674 675 676
  /// {@macro flutter.widgets.editableText.enableInteractiveSelection}
  final bool enableInteractiveSelection;

677 678 679
  /// {@macro flutter.widgets.scrollable.dragStartBehavior}
  final DragStartBehavior dragStartBehavior;

680
  /// {@macro flutter.widgets.editableText.selectionEnabled}
681
  bool get selectionEnabled => enableInteractiveSelection;
682

683 684
  /// {@template flutter.material.textfield.onTap}
  /// Called for each distinct tap except for every second tap of a double tap.
685
  ///
686
  /// The text field builds a [GestureDetector] to handle input events like tap,
687
  /// to trigger focus requests, to move the caret, adjust the selection, etc.
688
  /// Handling some of those events by wrapping the text field with a competing
689 690
  /// GestureDetector is problematic.
  ///
691
  /// To unconditionally handle taps, without interfering with the text field's
692 693
  /// internal gesture detector, provide this callback.
  ///
694
  /// If the text field is created with [enabled] false, taps will not be
695 696
  /// recognized.
  ///
697
  /// To be notified when the text field gains or loses the focus, provide a
698 699 700
  /// [focusNode] and add a listener to that.
  ///
  /// To listen to arbitrary pointer events without competing with the
701
  /// text field's internal gesture detector, use a [Listener].
702
  /// {@endtemplate}
703
  final GestureTapCallback? onTap;
704

705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721
  /// 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.
722
  final MouseCursor? mouseCursor;
723

724
  /// Callback that generates a custom [InputDecoration.counter] widget.
725 726 727
  ///
  /// See [InputCounterWidgetBuilder] for an explanation of the passed in
  /// arguments.  The returned widget will be placed below the line in place of
728
  /// the default widget built when [InputDecoration.counterText] is specified.
729 730
  ///
  /// The returned widget will be wrapped in a [Semantics] widget for
731 732
  /// accessibility, but it also needs to be accessible itself. For example,
  /// if returning a Text widget, set the [Text.semanticsLabel] property.
733
  ///
734
  /// {@tool snippet}
735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750
  /// ```dart
  /// Widget counter(
  ///   BuildContext context,
  ///   {
  ///     int currentLength,
  ///     int maxLength,
  ///     bool isFocused,
  ///   }
  /// ) {
  ///   return Text(
  ///     '$currentLength of $maxLength characters',
  ///     semanticsLabel: 'character count',
  ///   );
  /// }
  /// ```
  /// {@end-tool}
751 752 753
  ///
  /// If buildCounter returns null, then no counter and no Semantics widget will
  /// be created at all.
754
  final InputCounterWidgetBuilder? buildCounter;
755

Dan Field's avatar
Dan Field committed
756
  /// {@macro flutter.widgets.editableText.scrollPhysics}
757
  final ScrollPhysics? scrollPhysics;
758

759
  /// {@macro flutter.widgets.editableText.scrollController}
760
  final ScrollController? scrollController;
761

762
  /// {@macro flutter.widgets.editableText.autofillHints}
763
  /// {@macro flutter.services.autofill.autofillHints}
764
  final Iterable<String>? autofillHints;
765

766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782
  /// {@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}
783
  final String? restorationId;
784

785
  @override
786
  _TextFieldState createState() => _TextFieldState();
787 788

  @override
789 790
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
791 792
    properties.add(DiagnosticsProperty<TextEditingController>('controller', controller, defaultValue: null));
    properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode, defaultValue: null));
793
    properties.add(DiagnosticsProperty<bool>('enabled', enabled, defaultValue: null));
794
    properties.add(DiagnosticsProperty<InputDecoration>('decoration', decoration, defaultValue: const InputDecoration()));
795 796 797
    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));
798
    properties.add(DiagnosticsProperty<String>('obscuringCharacter', obscuringCharacter, defaultValue: '•'));
799
    properties.add(DiagnosticsProperty<bool>('obscureText', obscureText, defaultValue: false));
800
    properties.add(DiagnosticsProperty<bool>('autocorrect', autocorrect, defaultValue: true));
801 802
    properties.add(EnumProperty<SmartDashesType>('smartDashesType', smartDashesType, defaultValue: obscureText ? SmartDashesType.disabled : SmartDashesType.enabled));
    properties.add(EnumProperty<SmartQuotesType>('smartQuotesType', smartQuotesType, defaultValue: obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled));
803
    properties.add(DiagnosticsProperty<bool>('enableSuggestions', enableSuggestions, defaultValue: true));
804
    properties.add(IntProperty('maxLines', maxLines, defaultValue: 1));
805 806
    properties.add(IntProperty('minLines', minLines, defaultValue: null));
    properties.add(DiagnosticsProperty<bool>('expands', expands, defaultValue: false));
807
    properties.add(IntProperty('maxLength', maxLength, defaultValue: null));
808 809 810 811
    properties.add(FlagProperty('maxLengthEnforced', value: maxLengthEnforced, defaultValue: true, ifFalse: 'maxLength not enforced'));
    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));
812
    properties.add(DiagnosticsProperty<TextAlignVertical>('textAlignVertical', textAlignVertical, defaultValue: null));
813 814
    properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
    properties.add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0));
815
    properties.add(DoubleProperty('cursorHeight', cursorHeight, defaultValue: null));
816
    properties.add(DiagnosticsProperty<Radius>('cursorRadius', cursorRadius, defaultValue: null));
817
    properties.add(ColorProperty('cursorColor', cursorColor, defaultValue: null));
818 819 820
    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'));
821
    properties.add(DiagnosticsProperty<ScrollController>('scrollController', scrollController, defaultValue: null));
822
    properties.add(DiagnosticsProperty<ScrollPhysics>('scrollPhysics', scrollPhysics, defaultValue: null));
823 824 825
  }
}

826
class _TextFieldState extends State<TextField> with RestorationMixin implements TextSelectionGestureDetectorBuilderDelegate {
827 828
  RestorableTextEditingController? _controller;
  TextEditingController get _effectiveController => widget.controller ?? _controller!.value;
829

830
  FocusNode? _focusNode;
831
  FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode());
832

833 834
  bool _isHovering = false;

835 836
  bool get needsCounter => widget.maxLength != null
    && widget.decoration != null
837
    && widget.decoration!.counterText == null;
838

839 840
  bool _showSelectionHandles = false;

841
  late _TextFieldSelectionGestureDetectorBuilder _selectionGestureDetectorBuilder;
842 843 844

  // API for TextSelectionGestureDetectorBuilderDelegate.
  @override
845
  late bool forcePressEnabled;
846 847 848 849 850 851 852 853

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

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

854 855
  bool get _isEnabled =>  widget.enabled ?? widget.decoration?.enabled ?? true;

856
  int get _currentLength => _effectiveController.value.text.characters.length;
857

858
  bool get _hasIntrinsicError => widget.maxLength != null && widget.maxLength! > 0 && _effectiveController.value.text.characters.length > widget.maxLength!;
859 860 861

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

862
  InputDecoration _getEffectiveDecoration() {
863 864
    final MaterialLocalizations localizations = MaterialLocalizations.of(context)!;
    final ThemeData themeData = Theme.of(context)!;
865
    final InputDecoration effectiveDecoration = (widget.decoration ?? const InputDecoration())
866
      .applyDefaults(themeData.inputDecorationTheme)
867
      .copyWith(
868
        enabled: _isEnabled,
869
        hintMaxLines: widget.decoration?.hintMaxLines ?? widget.maxLines,
870
      );
871

872 873
    // No need to build anything if counter or counterText were given directly.
    if (effectiveDecoration.counter != null || effectiveDecoration.counterText != null)
874
      return effectiveDecoration;
875

876
    // If buildCounter was provided, use it to generate a counter widget.
877
    Widget? counter;
878
    final int currentLength = _currentLength;
879 880 881 882
    if (effectiveDecoration.counter == null
        && effectiveDecoration.counterText == null
        && widget.buildCounter != null) {
      final bool isFocused = _effectiveFocusNode.hasFocus;
883
      final Widget? builtCounter = widget.buildCounter!(
884 885 886 887
        context,
        currentLength: currentLength,
        maxLength: widget.maxLength,
        isFocused: isFocused,
888
      );
889 890 891 892 893 894 895 896
      // If buildCounter returns null, don't add a counter widget to the field.
      if (builtCounter != null) {
        counter = Semantics(
          container: true,
          liveRegion: isFocused,
          child: builtCounter,
        );
      }
897 898 899 900 901 902
      return effectiveDecoration.copyWith(counter: counter);
    }

    if (widget.maxLength == null)
      return effectiveDecoration; // No counter widget

903 904 905
    String counterText = '$currentLength';
    String semanticCounterText = '';

906
    // Handle a real maxLength (positive number)
907
    if (widget.maxLength! > 0) {
908
      // Show the maxLength in the counter
909
      counterText += '/${widget.maxLength}';
910
      final int remaining = (widget.maxLength! - currentLength).clamp(0, widget.maxLength!);
911
      semanticCounterText = localizations.remainingTextFieldCharacterCount(remaining);
912
    }
913

914 915 916 917
    if (_hasIntrinsicError) {
      return effectiveDecoration.copyWith(
        errorText: effectiveDecoration.errorText ?? '',
        counterStyle: effectiveDecoration.errorStyle
918
          ?? themeData.textTheme.caption!.copyWith(color: themeData.errorColor),
919 920 921
        counterText: counterText,
        semanticCounterText: semanticCounterText,
      );
922
    }
923

924 925 926 927
    return effectiveDecoration.copyWith(
      counterText: counterText,
      semanticCounterText: semanticCounterText,
    );
928 929
  }

930 931 932
  @override
  void initState() {
    super.initState();
933
    _selectionGestureDetectorBuilder = _TextFieldSelectionGestureDetectorBuilder(state: this);
934
    if (widget.controller == null) {
935
      _createLocalController();
936 937
    }
    _effectiveFocusNode.canRequestFocus = _isEnabled;
938 939
  }

940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955
  bool get _canRequestFocus {
    final NavigationMode mode = MediaQuery.of(context, nullOk: true)?.navigationMode ?? NavigationMode.traditional;
    switch (mode) {
      case NavigationMode.traditional:
        return _isEnabled;
      case NavigationMode.directional:
        return true;
    }
  }

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

956
  @override
957
  void didUpdateWidget(TextField oldWidget) {
958
    super.didUpdateWidget(oldWidget);
959
    if (widget.controller == null && oldWidget.controller != null) {
960
      _createLocalController(oldWidget.controller!.value);
961
    } else if (widget.controller != null && oldWidget.controller == null) {
962 963
      unregisterFromRestoration(_controller!);
      _controller!.dispose();
964
      _controller = null;
965
    }
966 967
    _effectiveFocusNode.canRequestFocus = _canRequestFocus;
    if (_effectiveFocusNode.hasFocus && widget.readOnly != oldWidget.readOnly && _isEnabled) {
968 969 970 971
      if(_effectiveController.selection.isCollapsed) {
        _showSelectionHandles = !widget.readOnly;
      }
    }
972 973
  }

974
  @override
975
  void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
976 977 978 979 980 981 982
    if (_controller != null) {
      _registerController();
    }
  }

  void _registerController() {
    assert(_controller != null);
983
    registerForRestoration(_controller!, 'controller');
984 985
  }

986
  void _createLocalController([TextEditingValue? value]) {
987 988 989 990 991 992 993 994 995 996
    assert(_controller == null);
    _controller = value == null
        ? RestorableTextEditingController()
        : RestorableTextEditingController.fromValue(value);
    if (!restorePending) {
      _registerController();
    }
  }

  @override
997
  String? get restorationId => widget.restorationId;
998

999 1000 1001
  @override
  void dispose() {
    _focusNode?.dispose();
1002
    _controller?.dispose();
1003 1004 1005
    super.dispose();
  }

1006
  EditableTextState? get _editableText => editableTextKey.currentState;
1007

1008
  void _requestKeyboard() {
1009 1010 1011
    _editableText?.requestKeyboard();
  }

1012
  bool _shouldShowSelectionHandles(SelectionChangedCause? cause) {
1013 1014
    // When the text field is activated by something that doesn't trigger the
    // selection overlay, we shouldn't show the handles either.
1015
    if (!_selectionGestureDetectorBuilder.shouldShowSelectionToolbar)
1016 1017 1018 1019 1020
      return false;

    if (cause == SelectionChangedCause.keyboard)
      return false;

1021 1022 1023
    if (widget.readOnly && _effectiveController.selection.isCollapsed)
      return false;

1024 1025 1026
    if (!_isEnabled)
      return false;

1027 1028 1029 1030 1031 1032 1033
    if (cause == SelectionChangedCause.longPress)
      return true;

    if (_effectiveController.text.isNotEmpty)
      return true;

    return false;
1034 1035
  }

1036
  void _handleSelectionChanged(TextSelection selection, SelectionChangedCause? cause) {
1037 1038 1039 1040 1041
    final bool willShowSelectionHandles = _shouldShowSelectionHandles(cause);
    if (willShowSelectionHandles != _showSelectionHandles) {
      setState(() {
        _showSelectionHandles = willShowSelectionHandles;
      });
1042 1043
    }

1044
    switch (Theme.of(context)!.platform) {
1045
      case TargetPlatform.iOS:
1046
      case TargetPlatform.macOS:
1047
        if (cause == SelectionChangedCause.longPress) {
1048
          _editableText?.bringIntoView(selection.base);
1049 1050 1051 1052
        }
        return;
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
1053 1054
      case TargetPlatform.linux:
      case TargetPlatform.windows:
1055 1056 1057 1058
        // Do nothing.
    }
  }

1059 1060 1061
  /// Toggle the toolbar when a selection handle is tapped.
  void _handleSelectionHandleTapped() {
    if (_effectiveController.selection.isCollapsed) {
1062
      _editableText!.toggleToolbar();
1063 1064 1065
    }
  }

1066 1067 1068
  void _handleHover(bool hovering) {
    if (hovering != _isHovering) {
      setState(() {
1069
        _isHovering = hovering;
1070 1071 1072 1073
      });
    }
  }

1074 1075
  @override
  Widget build(BuildContext context) {
1076
    assert(debugCheckHasMaterial(context));
1077
    assert(debugCheckHasMaterialLocalizations(context));
1078
    assert(debugCheckHasDirectionality(context));
1079
    assert(
1080 1081
      !(widget.style != null && widget.style!.inherit == false &&
        (widget.style!.fontSize == null || widget.style!.textBaseline == null)),
1082 1083 1084
      'inherit false style must supply fontSize and textBaseline',
    );

1085
    final ThemeData theme = Theme.of(context)!;
1086
    final TextSelectionThemeData selectionTheme = TextSelectionTheme.of(context);
1087
    final TextStyle style = theme.textTheme.subtitle1!.merge(widget.style);
1088
    final Brightness keyboardAppearance = widget.keyboardAppearance ?? theme.primaryColorBrightness;
1089 1090
    final TextEditingController controller = _effectiveController;
    final FocusNode focusNode = _effectiveFocusNode;
1091 1092
    final List<TextInputFormatter> formatters = widget.inputFormatters ?? <TextInputFormatter>[];
    if (widget.maxLength != null && widget.maxLengthEnforced)
1093
      formatters.add(LengthLimitingTextInputFormatter(widget.maxLength));
1094

1095
    TextSelectionControls textSelectionControls;
1096 1097
    bool paintCursorAboveText;
    bool cursorOpacityAnimates;
1098 1099
    Offset? cursorOffset;
    Color? cursorColor = widget.cursorColor;
1100
    Color selectionColor;
1101 1102
    Color? autocorrectionTextRectColor;
    Radius? cursorRadius = widget.cursorRadius;
1103

1104
    switch (theme.platform) {
1105
      case TargetPlatform.iOS:
1106
      case TargetPlatform.macOS:
1107
        final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context);
1108 1109
        forcePressEnabled = true;
        textSelectionControls = cupertinoTextSelectionControls;
1110 1111
        paintCursorAboveText = true;
        cursorOpacityAnimates = true;
1112 1113
        cursorColor ??= selectionTheme.cursorColor ?? cupertinoTheme.primaryColor;
        selectionColor = selectionTheme.selectionColor ?? cupertinoTheme.primaryColor.withOpacity(0.40);
1114
        cursorRadius ??= const Radius.circular(2.0);
1115
        cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.of(context)!.devicePixelRatio, 0);
1116
        autocorrectionTextRectColor = selectionColor;
1117
        break;
1118

1119 1120
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
1121 1122
      case TargetPlatform.linux:
      case TargetPlatform.windows:
1123 1124
        forcePressEnabled = false;
        textSelectionControls = materialTextSelectionControls;
1125 1126
        paintCursorAboveText = false;
        cursorOpacityAnimates = false;
1127 1128
        cursorColor ??= selectionTheme.cursorColor ?? theme.colorScheme.primary;
        selectionColor = selectionTheme.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40);
1129 1130 1131
        break;
    }

1132
    Widget child = RepaintBoundary(
1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 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
      child: UnmanagedRestorationScope(
        bucket: bucket,
        child: EditableText(
          key: editableTextKey,
          readOnly: widget.readOnly || !_isEnabled,
          toolbarOptions: widget.toolbarOptions,
          showCursor: widget.showCursor,
          showSelectionHandles: _showSelectionHandles,
          controller: controller,
          focusNode: focusNode,
          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,
          selectionColor: selectionColor,
          selectionControls: widget.selectionEnabled ? textSelectionControls : null,
          onChanged: widget.onChanged,
          onSelectionChanged: _handleSelectionChanged,
          onEditingComplete: widget.onEditingComplete,
          onSubmitted: widget.onSubmitted,
          onAppPrivateCommand: widget.onAppPrivateCommand,
          onSelectionHandleTapped: _handleSelectionHandleTapped,
          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,
          autofillHints: widget.autofillHints,
          autocorrectionTextRectColor: autocorrectionTextRectColor,
          restorationId: 'editable',
        ),
1191 1192 1193
      ),
    );

1194
    if (widget.decoration != null) {
1195 1196
      child = AnimatedBuilder(
        animation: Listenable.merge(<Listenable>[ focusNode, controller ]),
1197
        builder: (BuildContext context, Widget? child) {
1198
          return InputDecorator(
1199
            decoration: _getEffectiveDecoration(),
1200 1201
            baseStyle: widget.style,
            textAlign: widget.textAlign,
1202
            textAlignVertical: widget.textAlignVertical,
1203
            isHovering: _isHovering,
1204 1205
            isFocused: focusNode.hasFocus,
            isEmpty: controller.value.text.isEmpty,
1206
            expands: widget.expands,
1207 1208 1209 1210 1211 1212
            child: child,
          );
        },
        child: child,
      );
    }
1213 1214 1215 1216 1217 1218 1219 1220 1221 1222
    final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor>(
      widget.mouseCursor ?? MaterialStateMouseCursor.textable,
      <MaterialState>{
        if (!_isEnabled) MaterialState.disabled,
        if (_isHovering) MaterialState.hovered,
        if (focusNode.hasFocus) MaterialState.focused,
        if (_hasError) MaterialState.error,
      },
    );

1223 1224 1225 1226 1227 1228
    return MouseRegion(
      cursor: effectiveMouseCursor,
      onEnter: (PointerEnterEvent event) => _handleHover(true),
      onExit: (PointerExitEvent event) => _handleHover(false),
      child: IgnorePointer(
        ignoring: !_isEnabled,
1229 1230
        child: AnimatedBuilder(
          animation: controller, // changes the _currentLength
1231
          builder: (BuildContext context, Widget? child) {
1232
            return Semantics(
1233
              maxValueLength: widget.maxLengthEnforced && widget.maxLength != null && widget.maxLength! > 0
1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244
                  ? widget.maxLength
                  : null,
              currentValueLength: _currentLength,
              onTap: () {
                if (!_effectiveController.selection.isValid)
                  _effectiveController.selection = TextSelection.collapsed(offset: _effectiveController.text.length);
                _requestKeyboard();
              },
              child: child,
            );
          },
1245
          child: _selectionGestureDetectorBuilder.buildGestureDetector(
1246 1247 1248
            behavior: HitTestBehavior.translucent,
            child: child,
          ),
1249
        ),
1250
      ),
1251 1252
    );
  }
1253
}