text_field.dart 53.7 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 'debug.dart';
14
import 'desktop_text_selection.dart';
15
import 'feedback.dart';
16
import 'input_decorator.dart';
17
import 'material.dart';
18
import 'material_localizations.dart';
19
import 'material_state.dart';
20
import 'selectable_text.dart' show iOSHorizontalOffset;
21 22 23
import 'text_selection.dart';
import 'theme.dart';

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

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

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

  @override
  void onSingleTapUp(TapUpDetails details) {
    editableText.hideToolbar();
88
    super.onSingleTapUp(details);
89
    _state._requestKeyboard();
90
    _state.widget.onTap?.call();
91 92 93 94 95
  }

  @override
  void onSingleLongTapStart(LongPressStartDetails details) {
    if (delegate.selectionEnabled) {
96
      switch (Theme.of(_state.context).platform) {
97
        case TargetPlatform.iOS:
98
        case TargetPlatform.macOS:
99 100 101 102 103 104 105
          renderEditable.selectPositionAt(
            from: details.globalPosition,
            cause: SelectionChangedCause.longPress,
          );
          break;
        case TargetPlatform.android:
        case TargetPlatform.fuchsia:
106 107
        case TargetPlatform.linux:
        case TargetPlatform.windows:
108
          renderEditable.selectWord(cause: SelectionChangedCause.longPress);
109 110 111 112 113 114 115
          Feedback.forLongPress(_state.context);
          break;
      }
    }
  }
}

116
/// A Material Design text field.
117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138
///
/// 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
139
/// one of its ancestors to be a [Material] widget.
140 141 142 143
///
/// To integrate the [TextField] into a [Form] with other [FormField] widgets,
/// consider using [TextFormField].
///
144 145 146 147 148 149 150
/// {@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}
///
151 152 153
/// 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.
154
///
155
/// {@tool snippet}
156 157 158 159
/// 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.
///
160
/// ![](https://flutter.github.io/assets-for-api-docs/assets/material/text_field.png)
161
///
162
/// ```dart
163
/// const TextField(
164 165 166 167 168 169 170 171 172
///   obscureText: true,
///   decoration: InputDecoration(
///     border: OutlineInputBorder(),
///     labelText: 'Password',
///   ),
/// )
/// ```
/// {@end-tool}
///
173 174 175 176 177 178
/// ## 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.
///
179
/// {@tool dartpad}
180 181 182
/// This sample shows how to get a value from a TextField via the [onSubmitted]
/// callback.
///
183
/// ** See code in examples/api/lib/material/text_field/text_field.1.dart **
184 185
/// {@end-tool}
///
186 187
/// {@macro flutter.widgets.EditableText.lifeCycle}
///
188 189 190 191 192 193 194 195 196 197 198
/// 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].
199
///
200
/// ## Handling emojis and other complex characters
201
/// {@macro flutter.widgets.EditableText.onChanged}
202 203 204 205 206
///
/// 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.
207
///
208 209
/// {@macro flutter.widgets.editableText.showCaretOnScreen}
///
210 211
/// {@macro flutter.widgets.editableText.accessibility}
///
212 213 214 215 216 217
/// 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
218
///    [TextField]. The [EditableText] widget is rarely used directly unless
219
///    you are implementing an entirely different design language, such as
220
///    Cupertino.
221 222 223 224 225
///  * <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)
226 227 228 229 230 231 232 233 234
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.
235 236
  ///
  /// The [maxLines] property can be set to null to remove the restriction on
237
  /// the number of lines. By default, it is one, meaning this is a single-line
238
  /// text field. [maxLines] must not be zero.
239 240 241
  ///
  /// The [maxLength] property is set to null by default, which means the
  /// number of characters allowed in the text field is not restricted. If
242 243 244 245 246
  /// [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.
247 248
  ///
  /// After [maxLength] characters have been input, additional input
249 250 251 252 253
  /// 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.
254
  ///
255 256 257 258
  /// 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.
259
  ///
260 261 262
  /// The text cursor is not shown if [showCursor] is false or if [showCursor]
  /// is null (the default) and [readOnly] is true.
  ///
263 264 265 266 267
  /// 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.
  ///
268
  /// The [textAlign], [autofocus], [obscureText], [readOnly], [autocorrect],
269 270
  /// [scrollPadding], [maxLines], [maxLength], [selectionHeightStyle],
  /// [selectionWidthStyle], [enableSuggestions], and
271
  /// [enableIMEPersonalizedLearning] arguments must not be null.
272 273 274 275 276
  ///
  /// See also:
  ///
  ///  * [maxLength], which discusses the precise meaning of "number of
  ///    characters" and how it may differ from the intuitive meaning.
277
  const TextField({
278
    super.key,
279 280
    this.controller,
    this.focusNode,
281
    this.decoration = const InputDecoration(),
282
    TextInputType? keyboardType,
283
    this.textInputAction,
284
    this.textCapitalization = TextCapitalization.none,
285
    this.style,
286
    this.strutStyle,
287
    this.textAlign = TextAlign.start,
288
    this.textAlignVertical,
289
    this.textDirection,
290
    this.readOnly = false,
291
    ToolbarOptions? toolbarOptions,
292
    this.showCursor,
293
    this.autofocus = false,
294
    this.obscuringCharacter = '•',
295 296
    this.obscureText = false,
    this.autocorrect = true,
297 298
    SmartDashesType? smartDashesType,
    SmartQuotesType? smartQuotesType,
299
    this.enableSuggestions = true,
300
    this.maxLines = 1,
301 302
    this.minLines,
    this.expands = false,
303
    this.maxLength,
304
    this.maxLengthEnforcement,
305
    this.onChanged,
306
    this.onEditingComplete,
307
    this.onSubmitted,
308
    this.onAppPrivateCommand,
309
    this.inputFormatters,
310
    this.enabled,
311
    this.cursorWidth = 2.0,
312
    this.cursorHeight,
313 314
    this.cursorRadius,
    this.cursorColor,
315 316
    this.selectionHeightStyle = ui.BoxHeightStyle.tight,
    this.selectionWidthStyle = ui.BoxWidthStyle.tight,
317
    this.keyboardAppearance,
318
    this.scrollPadding = const EdgeInsets.all(20.0),
319
    this.dragStartBehavior = DragStartBehavior.start,
320
    bool? enableInteractiveSelection,
321
    this.selectionControls,
322
    this.onTap,
323
    this.mouseCursor,
324
    this.buildCounter,
325
    this.scrollController,
326
    this.scrollPhysics,
327
    this.autofillHints = const <String>[],
328
    this.clipBehavior = Clip.hardEdge,
329
    this.restorationId,
330
    this.scribbleEnabled = true,
331
    this.enableIMEPersonalizedLearning = true,
332
  }) : assert(textAlign != null),
333
       assert(readOnly != null),
334
       assert(autofocus != null),
335
       assert(obscuringCharacter != null && obscuringCharacter.length == 1),
336
       assert(obscureText != null),
337
       assert(autocorrect != null),
338 339
       smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
       smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
340
       assert(enableSuggestions != null),
341
       assert(scrollPadding != null),
342
       assert(dragStartBehavior != null),
343 344
       assert(selectionHeightStyle != null),
       assert(selectionWidthStyle != null),
345
       assert(maxLines == null || maxLines > 0),
346 347 348
       assert(minLines == null || minLines > 0),
       assert(
         (maxLines == null) || (minLines == null) || (maxLines >= minLines),
349
         "minLines can't be greater than maxLines",
350 351 352 353 354 355
       ),
       assert(expands != null),
       assert(
         !expands || (maxLines == null && minLines == null),
         'minLines and maxLines must be null when expands is true.',
       ),
356
       assert(!obscureText || maxLines == 1, 'Obscured fields cannot be multiline.'),
357
       assert(maxLength == null || maxLength == TextField.noMaxLength || maxLength > 0),
358
       // Assert the following instead of setting it directly to avoid surprising the user by silently changing the value they set.
359 360
       assert(
         !identical(textInputAction, TextInputAction.newline) ||
361 362
         maxLines == 1 ||
         !identical(keyboardType, TextInputType.text),
363 364
         'Use keyboardType TextInputType.multiline when using TextInputAction.newline on a multiline TextField.',
       ),
365
       assert(clipBehavior != null),
366
       assert(enableIMEPersonalizedLearning != null),
367
       keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391
       enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText),
       toolbarOptions = toolbarOptions ??
           (obscureText
               ? (readOnly
                   // No point in even offering "Select All" in a read-only obscured
                   // field.
                   ? const ToolbarOptions()
                   // Writable, but obscured.
                   : const ToolbarOptions(
                       selectAll: true,
                       paste: true,
                     ))
               : (readOnly
                   // Read-only, not obscured.
                   ? const ToolbarOptions(
                       selectAll: true,
                       copy: true,
                     )
                   // Writable, not obscured.
                   : const ToolbarOptions(
                       copy: true,
                       cut: true,
                       selectAll: true,
                       paste: true,
392
                     )));
393 394 395

  /// Controls the text being edited.
  ///
396
  /// If null, this widget will create its own [TextEditingController].
397
  final TextEditingController? controller;
398

399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418
  /// 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); });
  /// ```
419 420
  ///
  /// If null, this widget will create its own [FocusNode].
421 422 423
  ///
  /// ## Keyboard
  ///
Gary Qian's avatar
Gary Qian committed
424
  /// Requesting the focus will typically cause the keyboard to be shown
425 426
  /// if it's not showing already.
  ///
427
  /// On Android, the user can hide the keyboard - without changing the focus -
428 429 430 431 432 433 434 435 436
  /// 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()].
437
  final FocusNode? focusNode;
438 439 440

  /// The decoration to show around the text field.
  ///
441
  /// By default, draws a horizontal line under the text field but can be
442 443
  /// configured to show an icon, label, hint text, and error text.
  ///
444
  /// Specify null to remove the decoration entirely (including the
445
  /// extra padding introduced by the decoration to save space for the labels).
446
  final InputDecoration? decoration;
447

448
  /// {@macro flutter.widgets.editableText.keyboardType}
449 450
  final TextInputType keyboardType;

451 452
  /// The type of action button to use for the keyboard.
  ///
453 454
  /// Defaults to [TextInputAction.newline] if [keyboardType] is
  /// [TextInputType.multiline] and [TextInputAction.done] otherwise.
455
  final TextInputAction? textInputAction;
456

457
  /// {@macro flutter.widgets.editableText.textCapitalization}
458 459
  final TextCapitalization textCapitalization;

460 461 462 463
  /// The style to use for the text being edited.
  ///
  /// This text style is also used as the base style for the [decoration].
  ///
464
  /// If null, defaults to the `subtitle1` text style from the current [Theme].
465
  final TextStyle? style;
466

467
  /// {@macro flutter.widgets.editableText.strutStyle}
468
  final StrutStyle? strutStyle;
469

470
  /// {@macro flutter.widgets.editableText.textAlign}
471 472
  final TextAlign textAlign;

473
  /// {@macro flutter.material.InputDecorator.textAlignVertical}
474
  final TextAlignVertical? textAlignVertical;
475

476
  /// {@macro flutter.widgets.editableText.textDirection}
477
  final TextDirection? textDirection;
478

479
  /// {@macro flutter.widgets.editableText.autofocus}
480 481
  final bool autofocus;

482 483 484
  /// {@macro flutter.widgets.editableText.obscuringCharacter}
  final String obscuringCharacter;

485
  /// {@macro flutter.widgets.editableText.obscureText}
486 487
  final bool obscureText;

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

491
  /// {@macro flutter.services.TextInputConfiguration.smartDashesType}
492 493
  final SmartDashesType smartDashesType;

494
  /// {@macro flutter.services.TextInputConfiguration.smartQuotesType}
495 496
  final SmartQuotesType smartQuotesType;

497
  /// {@macro flutter.services.TextInputConfiguration.enableSuggestions}
498 499
  final bool enableSuggestions;

500
  /// {@macro flutter.widgets.editableText.maxLines}
501 502
  ///  * [expands], which determines whether the field should fill the height of
  ///    its parent.
503
  final int? maxLines;
504

505
  /// {@macro flutter.widgets.editableText.minLines}
506 507
  ///  * [expands], which determines whether the field should fill the height of
  ///    its parent.
508
  final int? minLines;
509 510 511 512

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

513 514 515
  /// {@macro flutter.widgets.editableText.readOnly}
  final bool readOnly;

516 517 518 519 520 521 522
  /// 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;

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

526 527
  /// If [maxLength] is set to this value, only the "current input length"
  /// part of the character counter is shown.
528
  static const int noMaxLength = -1;
529

530 531 532 533
  /// The maximum number of characters (Unicode scalar values) to allow in the
  /// text field.
  ///
  /// If set, a character counter will be displayed below the
534
  /// field showing how many characters have been entered. If set to a number
535
  /// greater than 0, it will also display the maximum number allowed. If set
536 537 538
  /// to [TextField.noMaxLength] then only the current character count is displayed.
  ///
  /// After [maxLength] characters have been input, additional input
539 540 541 542 543
  /// 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.
544
  ///
545 546 547 548
  /// 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.
549 550 551 552
  ///
  /// Whitespace characters (e.g. newline, space, tab) are included in the
  /// character count.
  ///
553 554 555 556
  /// 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.
557
  ///
558
  /// {@macro flutter.services.lengthLimitingTextInputFormatter.maxLength}
559
  final int? maxLength;
560

561 562 563 564 565 566 567
  /// Determines how the [maxLength] limit should be enforced.
  ///
  /// {@macro flutter.services.textFormatter.effectiveMaxLengthEnforcement}
  ///
  /// {@macro flutter.services.textFormatter.maxLengthEnforcement}
  final MaxLengthEnforcement? maxLengthEnforcement;

568
  /// {@macro flutter.widgets.editableText.onChanged}
569 570 571 572 573
  ///
  /// See also:
  ///
  ///  * [inputFormatters], which are called before [onChanged]
  ///    runs and can validate and change ("format") the input value.
574
  ///  * [onEditingComplete], [onSubmitted]:
575
  ///    which are more specialized input change notifications.
576
  final ValueChanged<String>? onChanged;
577

578
  /// {@macro flutter.widgets.editableText.onEditingComplete}
579
  final VoidCallback? onEditingComplete;
580

581
  /// {@macro flutter.widgets.editableText.onSubmitted}
582 583 584
  ///
  /// See also:
  ///
585 586 587
  ///  * [TextInputAction.next] and [TextInputAction.previous], which
  ///    automatically shift the focus to the next/previous focusable item when
  ///    the user is done editing.
588
  final ValueChanged<String>? onSubmitted;
589

590
  /// {@macro flutter.widgets.editableText.onAppPrivateCommand}
591
  final AppPrivateCommandCallback? onAppPrivateCommand;
592

593
  /// {@macro flutter.widgets.editableText.inputFormatters}
594
  final List<TextInputFormatter>? inputFormatters;
595

596
  /// If false the text field is "disabled": it ignores taps and its
597 598 599
  /// [decoration] is rendered in grey.
  ///
  /// If non-null this property overrides the [decoration]'s
600
  /// [InputDecoration.enabled] property.
601
  final bool? enabled;
602

603
  /// {@macro flutter.widgets.editableText.cursorWidth}
604 605
  final double cursorWidth;

606
  /// {@macro flutter.widgets.editableText.cursorHeight}
607
  final double? cursorHeight;
608

609
  /// {@macro flutter.widgets.editableText.cursorRadius}
610
  final Radius? cursorRadius;
611

612
  /// The color of the cursor.
613
  ///
614 615 616
  /// The cursor indicates the current location of text insertion point in
  /// the field.
  ///
617
  /// If this is null it will default to the ambient
618
  /// [DefaultSelectionStyle.cursorColor]. If that is null, and the
619 620 621
  /// [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].
622
  final Color? cursorColor;
623

624 625 626 627 628 629 630 631 632 633
  /// 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;

634
  /// The appearance of the keyboard.
635
  ///
636
  /// This setting is only honored on iOS devices.
637
  ///
638
  /// If unset, defaults to [ThemeData.brightness].
639
  final Brightness? keyboardAppearance;
640

641
  /// {@macro flutter.widgets.editableText.scrollPadding}
642 643
  final EdgeInsets scrollPadding;

644 645 646
  /// {@macro flutter.widgets.editableText.enableInteractiveSelection}
  final bool enableInteractiveSelection;

647 648 649
  /// {@macro flutter.widgets.editableText.selectionControls}
  final TextSelectionControls? selectionControls;

650 651 652
  /// {@macro flutter.widgets.scrollable.dragStartBehavior}
  final DragStartBehavior dragStartBehavior;

653
  /// {@macro flutter.widgets.editableText.selectionEnabled}
654
  bool get selectionEnabled => enableInteractiveSelection;
655

656 657
  /// {@template flutter.material.textfield.onTap}
  /// Called for each distinct tap except for every second tap of a double tap.
658
  ///
659
  /// The text field builds a [GestureDetector] to handle input events like tap,
660
  /// to trigger focus requests, to move the caret, adjust the selection, etc.
661
  /// Handling some of those events by wrapping the text field with a competing
662 663
  /// GestureDetector is problematic.
  ///
664
  /// To unconditionally handle taps, without interfering with the text field's
665 666
  /// internal gesture detector, provide this callback.
  ///
667
  /// If the text field is created with [enabled] false, taps will not be
668 669
  /// recognized.
  ///
670
  /// To be notified when the text field gains or loses the focus, provide a
671 672 673
  /// [focusNode] and add a listener to that.
  ///
  /// To listen to arbitrary pointer events without competing with the
674
  /// text field's internal gesture detector, use a [Listener].
675
  /// {@endtemplate}
676
  final GestureTapCallback? onTap;
677

678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694
  /// 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.
695
  final MouseCursor? mouseCursor;
696

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

Dan Field's avatar
Dan Field committed
729
  /// {@macro flutter.widgets.editableText.scrollPhysics}
730
  final ScrollPhysics? scrollPhysics;
731

732
  /// {@macro flutter.widgets.editableText.scrollController}
733
  final ScrollController? scrollController;
734

735
  /// {@macro flutter.widgets.editableText.autofillHints}
736
  /// {@macro flutter.services.AutofillConfiguration.autofillHints}
737
  final Iterable<String>? autofillHints;
738

739 740 741 742 743
  /// {@macro flutter.material.Material.clipBehavior}
  ///
  /// Defaults to [Clip.hardEdge].
  final Clip clipBehavior;

744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760
  /// {@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}
761
  final String? restorationId;
762

763 764 765
  /// {@macro flutter.widgets.editableText.scribbleEnabled}
  final bool scribbleEnabled;

766 767 768
  /// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
  final bool enableIMEPersonalizedLearning;

769
  @override
770
  State<TextField> createState() => _TextFieldState();
771 772

  @override
773 774
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
775 776
    properties.add(DiagnosticsProperty<TextEditingController>('controller', controller, defaultValue: null));
    properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode, defaultValue: null));
777
    properties.add(DiagnosticsProperty<bool>('enabled', enabled, defaultValue: null));
778
    properties.add(DiagnosticsProperty<InputDecoration>('decoration', decoration, defaultValue: const InputDecoration()));
779 780 781
    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));
782
    properties.add(DiagnosticsProperty<String>('obscuringCharacter', obscuringCharacter, defaultValue: '•'));
783
    properties.add(DiagnosticsProperty<bool>('obscureText', obscureText, defaultValue: false));
784
    properties.add(DiagnosticsProperty<bool>('autocorrect', autocorrect, defaultValue: true));
785 786
    properties.add(EnumProperty<SmartDashesType>('smartDashesType', smartDashesType, defaultValue: obscureText ? SmartDashesType.disabled : SmartDashesType.enabled));
    properties.add(EnumProperty<SmartQuotesType>('smartQuotesType', smartQuotesType, defaultValue: obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled));
787
    properties.add(DiagnosticsProperty<bool>('enableSuggestions', enableSuggestions, defaultValue: true));
788
    properties.add(IntProperty('maxLines', maxLines, defaultValue: 1));
789 790
    properties.add(IntProperty('minLines', minLines, defaultValue: null));
    properties.add(DiagnosticsProperty<bool>('expands', expands, defaultValue: false));
791
    properties.add(IntProperty('maxLength', maxLength, defaultValue: null));
792
    properties.add(EnumProperty<MaxLengthEnforcement>('maxLengthEnforcement', maxLengthEnforcement, defaultValue: null));
793 794 795
    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));
796
    properties.add(DiagnosticsProperty<TextAlignVertical>('textAlignVertical', textAlignVertical, defaultValue: null));
797 798
    properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
    properties.add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0));
799
    properties.add(DoubleProperty('cursorHeight', cursorHeight, defaultValue: null));
800
    properties.add(DiagnosticsProperty<Radius>('cursorRadius', cursorRadius, defaultValue: null));
801
    properties.add(ColorProperty('cursorColor', cursorColor, defaultValue: null));
802 803 804
    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'));
805
    properties.add(DiagnosticsProperty<TextSelectionControls>('selectionControls', selectionControls, defaultValue: null));
806
    properties.add(DiagnosticsProperty<ScrollController>('scrollController', scrollController, defaultValue: null));
807
    properties.add(DiagnosticsProperty<ScrollPhysics>('scrollPhysics', scrollPhysics, defaultValue: null));
808
    properties.add(DiagnosticsProperty<Clip>('clipBehavior', clipBehavior, defaultValue: Clip.hardEdge));
809
    properties.add(DiagnosticsProperty<bool>('scribbleEnabled', scribbleEnabled, defaultValue: true));
810
    properties.add(DiagnosticsProperty<bool>('enableIMEPersonalizedLearning', enableIMEPersonalizedLearning, defaultValue: true));
811 812 813
  }
}

814
class _TextFieldState extends State<TextField> with RestorationMixin implements TextSelectionGestureDetectorBuilderDelegate, AutofillClient {
815 816
  RestorableTextEditingController? _controller;
  TextEditingController get _effectiveController => widget.controller ?? _controller!.value;
817

818
  FocusNode? _focusNode;
819
  FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode());
820

821
  MaxLengthEnforcement get _effectiveMaxLengthEnforcement => widget.maxLengthEnforcement
822
    ?? LengthLimitingTextInputFormatter.getDefaultMaxLengthEnforcement(Theme.of(context).platform);
823

824 825
  bool _isHovering = false;

826 827
  bool get needsCounter => widget.maxLength != null
    && widget.decoration != null
828
    && widget.decoration!.counterText == null;
829

830 831
  bool _showSelectionHandles = false;

832
  late _TextFieldSelectionGestureDetectorBuilder _selectionGestureDetectorBuilder;
833 834 835

  // API for TextSelectionGestureDetectorBuilderDelegate.
  @override
836
  late bool forcePressEnabled;
837 838 839 840 841 842 843 844

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

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

845 846
  bool get _isEnabled =>  widget.enabled ?? widget.decoration?.enabled ?? true;

847
  int get _currentLength => _effectiveController.value.text.characters.length;
848

849
  bool get _hasIntrinsicError => widget.maxLength != null && widget.maxLength! > 0 && _effectiveController.value.text.characters.length > widget.maxLength!;
850 851 852

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

853
  InputDecoration _getEffectiveDecoration() {
854
    final MaterialLocalizations localizations = MaterialLocalizations.of(context);
855
    final ThemeData themeData = Theme.of(context);
856
    final InputDecoration effectiveDecoration = (widget.decoration ?? const InputDecoration())
857
      .applyDefaults(themeData.inputDecorationTheme)
858
      .copyWith(
859
        enabled: _isEnabled,
860
        hintMaxLines: widget.decoration?.hintMaxLines ?? widget.maxLines,
861
      );
862

863
    // No need to build anything if counter or counterText were given directly.
864
    if (effectiveDecoration.counter != null || effectiveDecoration.counterText != null) {
865
      return effectiveDecoration;
866
    }
867

868
    // If buildCounter was provided, use it to generate a counter widget.
869
    Widget? counter;
870
    final int currentLength = _currentLength;
871 872 873 874
    if (effectiveDecoration.counter == null
        && effectiveDecoration.counterText == null
        && widget.buildCounter != null) {
      final bool isFocused = _effectiveFocusNode.hasFocus;
875
      final Widget? builtCounter = widget.buildCounter!(
876 877 878 879
        context,
        currentLength: currentLength,
        maxLength: widget.maxLength,
        isFocused: isFocused,
880
      );
881 882 883 884 885 886 887 888
      // If buildCounter returns null, don't add a counter widget to the field.
      if (builtCounter != null) {
        counter = Semantics(
          container: true,
          liveRegion: isFocused,
          child: builtCounter,
        );
      }
889 890 891
      return effectiveDecoration.copyWith(counter: counter);
    }

892 893 894
    if (widget.maxLength == null) {
      return effectiveDecoration;
    } // No counter widget
895

896 897 898
    String counterText = '$currentLength';
    String semanticCounterText = '';

899
    // Handle a real maxLength (positive number)
900
    if (widget.maxLength! > 0) {
901
      // Show the maxLength in the counter
902
      counterText += '/${widget.maxLength}';
903
      final int remaining = (widget.maxLength! - currentLength).clamp(0, widget.maxLength!); // ignore_clamp_double_lint
904
      semanticCounterText = localizations.remainingTextFieldCharacterCount(remaining);
905
    }
906

907 908 909 910
    if (_hasIntrinsicError) {
      return effectiveDecoration.copyWith(
        errorText: effectiveDecoration.errorText ?? '',
        counterStyle: effectiveDecoration.errorStyle
911
          ?? themeData.textTheme.caption!.copyWith(color: themeData.errorColor),
912 913 914
        counterText: counterText,
        semanticCounterText: semanticCounterText,
      );
915
    }
916

917 918 919 920
    return effectiveDecoration.copyWith(
      counterText: counterText,
      semanticCounterText: semanticCounterText,
    );
921 922
  }

923 924 925
  @override
  void initState() {
    super.initState();
926
    _selectionGestureDetectorBuilder = _TextFieldSelectionGestureDetectorBuilder(state: this);
927
    if (widget.controller == null) {
928
      _createLocalController();
929 930
    }
    _effectiveFocusNode.canRequestFocus = _isEnabled;
931
    _effectiveFocusNode.addListener(_handleFocusChanged);
932 933
  }

934
  bool get _canRequestFocus {
935
    final NavigationMode mode = MediaQuery.maybeOf(context)?.navigationMode ?? NavigationMode.traditional;
936 937 938 939 940 941 942 943 944 945 946 947 948 949
    switch (mode) {
      case NavigationMode.traditional:
        return _isEnabled;
      case NavigationMode.directional:
        return true;
    }
  }

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

950
  @override
951
  void didUpdateWidget(TextField oldWidget) {
952
    super.didUpdateWidget(oldWidget);
953
    if (widget.controller == null && oldWidget.controller != null) {
954
      _createLocalController(oldWidget.controller!.value);
955
    } else if (widget.controller != null && oldWidget.controller == null) {
956 957
      unregisterFromRestoration(_controller!);
      _controller!.dispose();
958
      _controller = null;
959
    }
960 961 962 963 964 965

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

966
    _effectiveFocusNode.canRequestFocus = _canRequestFocus;
967

968
    if (_effectiveFocusNode.hasFocus && widget.readOnly != oldWidget.readOnly && _isEnabled) {
969 970 971 972
      if(_effectiveController.selection.isCollapsed) {
        _showSelectionHandles = !widget.readOnly;
      }
    }
973 974
  }

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

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

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

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

1000 1001
  @override
  void dispose() {
1002
    _effectiveFocusNode.removeListener(_handleFocusChanged);
1003
    _focusNode?.dispose();
1004
    _controller?.dispose();
1005 1006 1007
    super.dispose();
  }

1008
  EditableTextState? get _editableText => editableTextKey.currentState;
1009

1010
  void _requestKeyboard() {
1011 1012 1013
    _editableText?.requestKeyboard();
  }

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

1021
    if (cause == SelectionChangedCause.keyboard) {
1022
      return false;
1023
    }
1024

1025
    if (widget.readOnly && _effectiveController.selection.isCollapsed) {
1026
      return false;
1027
    }
1028

1029
    if (!_isEnabled) {
1030
      return false;
1031
    }
1032

1033
    if (cause == SelectionChangedCause.longPress || cause == SelectionChangedCause.scribble) {
1034
      return true;
1035
    }
1036

1037
    if (_effectiveController.text.isNotEmpty) {
1038
      return true;
1039
    }
1040 1041

    return false;
1042 1043
  }

1044 1045 1046 1047 1048 1049 1050
  void _handleFocusChanged() {
    setState(() {
      // Rebuild the widget on focus change to show/hide the text selection
      // highlight.
    });
  }

1051
  void _handleSelectionChanged(TextSelection selection, SelectionChangedCause? cause) {
1052 1053 1054 1055 1056
    final bool willShowSelectionHandles = _shouldShowSelectionHandles(cause);
    if (willShowSelectionHandles != _showSelectionHandles) {
      setState(() {
        _showSelectionHandles = willShowSelectionHandles;
      });
1057 1058
    }

1059
    switch (Theme.of(context).platform) {
1060
      case TargetPlatform.iOS:
1061
      case TargetPlatform.macOS:
1062 1063 1064
        if (cause == SelectionChangedCause.longPress
            || cause == SelectionChangedCause.drag) {
          _editableText?.bringIntoView(selection.extent);
1065
        }
1066
        break;
1067 1068
      case TargetPlatform.linux:
      case TargetPlatform.windows:
1069 1070 1071 1072 1073
      case TargetPlatform.fuchsia:
      case TargetPlatform.android:
        if (cause == SelectionChangedCause.drag) {
          _editableText?.bringIntoView(selection.extent);
        }
1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088
        break;
    }

    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();
        }
        break;
1089 1090 1091
    }
  }

1092 1093 1094
  /// Toggle the toolbar when a selection handle is tapped.
  void _handleSelectionHandleTapped() {
    if (_effectiveController.selection.isCollapsed) {
1095
      _editableText!.toggleToolbar();
1096 1097 1098
    }
  }

1099 1100 1101
  void _handleHover(bool hovering) {
    if (hovering != _isHovering) {
      setState(() {
1102
        _isHovering = hovering;
1103 1104 1105 1106
      });
    }
  }

1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129
  // 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.

1130 1131
  @override
  Widget build(BuildContext context) {
1132
    assert(debugCheckHasMaterial(context));
1133
    assert(debugCheckHasMaterialLocalizations(context));
1134
    assert(debugCheckHasDirectionality(context));
1135
    assert(
1136 1137
      !(widget.style != null && widget.style!.inherit == false &&
        (widget.style!.fontSize == null || widget.style!.textBaseline == null)),
1138 1139 1140
      'inherit false style must supply fontSize and textBaseline',
    );

1141
    final ThemeData theme = Theme.of(context);
1142
    final DefaultSelectionStyle selectionStyle = DefaultSelectionStyle.of(context);
1143
    final TextStyle style = theme.textTheme.subtitle1!.merge(widget.style);
1144
    final Brightness keyboardAppearance = widget.keyboardAppearance ?? theme.brightness;
1145 1146
    final TextEditingController controller = _effectiveController;
    final FocusNode focusNode = _effectiveFocusNode;
1147 1148
    final List<TextInputFormatter> formatters = <TextInputFormatter>[
      ...?widget.inputFormatters,
1149
      if (widget.maxLength != null)
1150 1151 1152 1153
        LengthLimitingTextInputFormatter(
          widget.maxLength,
          maxLengthEnforcement: _effectiveMaxLengthEnforcement,
        ),
1154
    ];
1155

1156
    TextSelectionControls? textSelectionControls = widget.selectionControls;
1157 1158
    final bool paintCursorAboveText;
    final bool cursorOpacityAnimates;
1159
    Offset? cursorOffset;
1160
    final Color cursorColor;
1161
    final Color selectionColor;
1162 1163
    Color? autocorrectionTextRectColor;
    Radius? cursorRadius = widget.cursorRadius;
1164
    VoidCallback? handleDidGainAccessibilityFocus;
1165

1166
    switch (theme.platform) {
1167
      case TargetPlatform.iOS:
1168
        final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context);
1169
        forcePressEnabled = true;
1170
        textSelectionControls ??= cupertinoTextSelectionControls;
1171 1172
        paintCursorAboveText = true;
        cursorOpacityAnimates = true;
1173 1174
        cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? cupertinoTheme.primaryColor;
        selectionColor = selectionStyle.selectionColor ?? cupertinoTheme.primaryColor.withOpacity(0.40);
1175
        cursorRadius ??= const Radius.circular(2.0);
1176
        cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0);
1177
        autocorrectionTextRectColor = selectionColor;
1178
        break;
1179

1180 1181 1182 1183 1184
      case TargetPlatform.macOS:
        final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context);
        forcePressEnabled = false;
        textSelectionControls ??= cupertinoDesktopTextSelectionControls;
        paintCursorAboveText = true;
1185
        cursorOpacityAnimates = false;
1186 1187
        cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? cupertinoTheme.primaryColor;
        selectionColor = selectionStyle.selectionColor ?? cupertinoTheme.primaryColor.withOpacity(0.40);
1188 1189
        cursorRadius ??= const Radius.circular(2.0);
        cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0);
1190
        handleDidGainAccessibilityFocus = () {
1191
          // Automatically activate the TextField when it receives accessibility focus.
1192 1193 1194 1195
          if (!_effectiveFocusNode.hasFocus && _effectiveFocusNode.canRequestFocus) {
            _effectiveFocusNode.requestFocus();
          }
        };
1196 1197
        break;

1198 1199
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
1200 1201 1202 1203
        forcePressEnabled = false;
        textSelectionControls ??= materialTextSelectionControls;
        paintCursorAboveText = false;
        cursorOpacityAnimates = false;
1204 1205
        cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? theme.colorScheme.primary;
        selectionColor = selectionStyle.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40);
1206 1207
        break;

1208
      case TargetPlatform.linux:
1209 1210 1211 1212
        forcePressEnabled = false;
        textSelectionControls ??= desktopTextSelectionControls;
        paintCursorAboveText = false;
        cursorOpacityAnimates = false;
1213 1214
        cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? theme.colorScheme.primary;
        selectionColor = selectionStyle.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40);
1215 1216
        break;

1217
      case TargetPlatform.windows:
1218
        forcePressEnabled = false;
1219
        textSelectionControls ??= desktopTextSelectionControls;
1220 1221
        paintCursorAboveText = false;
        cursorOpacityAnimates = false;
1222 1223
        cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? theme.colorScheme.primary;
        selectionColor = selectionStyle.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40);
1224 1225 1226 1227 1228 1229
        handleDidGainAccessibilityFocus = () {
          // Automatically activate the TextField when it receives accessibility focus.
          if (!_effectiveFocusNode.hasFocus && _effectiveFocusNode.canRequestFocus) {
            _effectiveFocusNode.requestFocus();
          }
        };
1230 1231 1232
        break;
    }

1233
    Widget child = RepaintBoundary(
1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260
      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,
1261 1262
          // Only show the selection highlight when the text field is focused.
          selectionColor: focusNode.hasFocus ? selectionColor : null,
1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288
          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,
1289
          autofillClient: this,
1290
          autocorrectionTextRectColor: autocorrectionTextRectColor,
1291
          clipBehavior: widget.clipBehavior,
1292
          restorationId: 'editable',
1293
          scribbleEnabled: widget.scribbleEnabled,
1294
          enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,
1295
        ),
1296 1297 1298
      ),
    );

1299
    if (widget.decoration != null) {
1300 1301
      child = AnimatedBuilder(
        animation: Listenable.merge(<Listenable>[ focusNode, controller ]),
1302
        builder: (BuildContext context, Widget? child) {
1303
          return InputDecorator(
1304
            decoration: _getEffectiveDecoration(),
1305 1306
            baseStyle: widget.style,
            textAlign: widget.textAlign,
1307
            textAlignVertical: widget.textAlignVertical,
1308
            isHovering: _isHovering,
1309 1310
            isFocused: focusNode.hasFocus,
            isEmpty: controller.value.text.isEmpty,
1311
            expands: widget.expands,
1312 1313 1314 1315 1316 1317
            child: child,
          );
        },
        child: child,
      );
    }
1318 1319 1320 1321 1322 1323 1324 1325 1326 1327
    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,
      },
    );

1328
    final int? semanticsMaxValueLength;
1329
    if (_effectiveMaxLengthEnforcement != MaxLengthEnforcement.none &&
1330 1331 1332 1333 1334 1335 1336
      widget.maxLength != null &&
      widget.maxLength! > 0) {
      semanticsMaxValueLength = widget.maxLength;
    } else {
      semanticsMaxValueLength = null;
    }

1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351
    return FocusTrapArea(
      focusNode: focusNode,
      child: MouseRegion(
        cursor: effectiveMouseCursor,
        onEnter: (PointerEnterEvent event) => _handleHover(true),
        onExit: (PointerExitEvent event) => _handleHover(false),
        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 : () {
1352
                  if (!_effectiveController.selection.isValid) {
1353
                    _effectiveController.selection = TextSelection.collapsed(offset: _effectiveController.text.length);
1354
                  }
1355 1356 1357 1358 1359 1360 1361 1362
                  _requestKeyboard();
                },
                onDidGainAccessibilityFocus: handleDidGainAccessibilityFocus,
                child: child,
              );
            },
            child: _selectionGestureDetectorBuilder.buildGestureDetector(
              behavior: HitTestBehavior.translucent,
1363
              child: child,
1364
            ),
1365
          ),
1366
        ),
1367
      ),
1368 1369
    );
  }
1370
}