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

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

7
import 'package:flutter/foundation.dart' show defaultTargetPlatform;
8
import 'package:flutter/gestures.dart';
9 10 11 12
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';

13
import 'adaptive_text_selection_toolbar.dart';
14
import 'colors.dart';
15
import 'desktop_text_selection.dart';
16
import 'icons.dart';
17
import 'magnifier.dart';
18
import 'spell_check_suggestions_toolbar.dart';
19
import 'text_selection.dart';
xster's avatar
xster committed
20
import 'theme.dart';
21

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

24 25 26 27 28
const TextStyle _kDefaultPlaceholderStyle = TextStyle(
  fontWeight: FontWeight.w400,
  color: CupertinoColors.placeholderText,
);

29
// Value inspected from Xcode 11 & iOS 13.0 Simulator.
30
const BorderSide _kDefaultRoundedBorderSide = BorderSide(
31 32 33 34
  color: CupertinoDynamicColor.withBrightness(
    color: Color(0x33000000),
    darkColor: Color(0x33FFFFFF),
  ),
35 36 37 38 39 40 41 42
  width: 0.0,
);
const Border _kDefaultRoundedBorder = Border(
  top: _kDefaultRoundedBorderSide,
  bottom: _kDefaultRoundedBorderSide,
  left: _kDefaultRoundedBorderSide,
  right: _kDefaultRoundedBorderSide,
);
43

44
const BoxDecoration _kDefaultRoundedBorderDecoration = BoxDecoration(
45 46 47 48
  color: CupertinoDynamicColor.withBrightness(
    color: CupertinoColors.white,
    darkColor: CupertinoColors.black,
  ),
49
  border: _kDefaultRoundedBorder,
50 51 52 53 54 55
  borderRadius: BorderRadius.all(Radius.circular(5.0)),
);

const Color _kDisabledBackground = CupertinoDynamicColor.withBrightness(
  color: Color(0xFFFAFAFA),
  darkColor: Color(0xFF050505),
56 57
);

58
// Value inspected from Xcode 12 & iOS 14.0 Simulator.
59 60
// Note it may not be consistent with https://developer.apple.com/design/resources/.
const CupertinoDynamicColor _kClearButtonColor = CupertinoDynamicColor.withBrightness(
61 62
  color: Color(0x33000000),
  darkColor: Color(0x33FFFFFF),
63
);
64

65 66 67 68 69 70 71 72
// An eyeballed value that moves the cursor slightly left of where it is
// rendered for text on Android so it's positioning more accurately matches the
// native iOS text cursor positioning.
//
// This value is in device pixels, not logical pixels as is typically used
// throughout the codebase.
const int _iOSHorizontalCursorOffsetPixels = -2;

73 74 75 76 77 78 79 80 81 82
/// Visibility of text field overlays based on the state of the current text entry.
///
/// Used to toggle the visibility behavior of the optional decorating widgets
/// surrounding the [EditableText] such as the clear text button.
enum OverlayVisibilityMode {
  /// Overlay will never appear regardless of the text entry state.
  never,

  /// Overlay will only appear when the current text entry is not empty.
  ///
83
  /// This includes prefilled text that the user did not type in manually. But
84 85 86 87 88
  /// does not include text in placeholders.
  editing,

  /// Overlay will only appear when the current text entry is empty.
  ///
89
  /// This also includes not having prefilled text that the user did not type
90 91 92 93 94 95 96
  /// in manually. Texts in placeholders are ignored.
  notEditing,

  /// Always show the overlay regardless of the text entry state.
  always,
}

97 98
class _CupertinoTextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDetectorBuilder {
  _CupertinoTextFieldSelectionGestureDetectorBuilder({
99
    required _CupertinoTextFieldState state,
100 101 102 103 104 105
  }) : _state = state,
       super(delegate: state);

  final _CupertinoTextFieldState _state;

  @override
106
  void onSingleTapUp(TapDragUpDetails details) {
107 108
    // Because TextSelectionGestureDetector listens to taps that happen on
    // widgets in front of it, tapping the clear button will also trigger
Shi-Hao Hong's avatar
Shi-Hao Hong committed
109
    // this handler. If the clear button widget recognizes the up event,
110 111
    // then do not handle it.
    if (_state._clearGlobalKey.currentContext != null) {
112
      final RenderBox renderBox = _state._clearGlobalKey.currentContext!.findRenderObject()! as RenderBox;
113 114 115 116 117 118 119
      final Offset localOffset = renderBox.globalToLocal(details.globalPosition);
      if (renderBox.hitTest(BoxHitTestResult(), position: localOffset)) {
        return;
      }
    }
    super.onSingleTapUp(details);
    _state._requestKeyboard();
120
    _state.widget.onTap?.call();
121 122 123
  }

  @override
124
  void onDragSelectionEnd(TapDragEndDetails details) {
125
    _state._requestKeyboard();
126
    super.onDragSelectionEnd(details);
127 128 129
  }
}

130 131 132 133 134 135 136 137 138 139 140 141 142
/// An iOS-style text field.
///
/// A text field lets the user enter text, either with a hardware keyboard or with
/// an onscreen keyboard.
///
/// This widget corresponds to both a `UITextField` and an editable `UITextView`
/// on iOS.
///
/// 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.
///
143
/// {@macro flutter.widgets.EditableText.onChanged}
144
///
145
/// {@tool dartpad}
146
/// This example shows how to set the initial value of the [CupertinoTextField] using
147
/// a [controller] that already contains some text.
148
///
149
/// ** See code in examples/api/lib/cupertino/text_field/cupertino_text_field.0.dart **
150
/// {@end-tool}
151 152 153 154 155 156 157 158
///
/// The [controller] can also control the selection and composing region (and to
/// observe changes to the text, selection, and composing region).
///
/// The text field has an overridable [decoration] that, by default, draws a
/// rounded rectangle border around the text field. If you set the [decoration]
/// property to null, the decoration will be removed entirely.
///
159 160
/// {@macro flutter.material.textfield.wantKeepAlive}
///
Dan Field's avatar
Dan Field committed
161 162
/// Remember to call [TextEditingController.dispose] when it is no longer
/// needed. This will ensure we discard any resources used by the object.
163
///
164 165
/// {@macro flutter.widgets.editableText.showCaretOnScreen}
///
166 167 168 169 170 171 172
/// See also:
///
///  * <https://developer.apple.com/documentation/uikit/uitextfield>
///  * [TextField], an alternative text field widget that follows the Material
///    Design UI conventions.
///  * [EditableText], which is the raw text editing control at the heart of a
///    [TextField].
173
///  * Learn how to use a [TextEditingController] in one of our [cookbook recipes](https://flutter.dev/docs/cookbook/forms/text-field-changes#2-use-a-texteditingcontroller).
174
///  * <https://developer.apple.com/design/human-interface-guidelines/ios/controls/text-fields/>
175 176 177
class CupertinoTextField extends StatefulWidget {
  /// Creates an iOS-style text field.
  ///
178
  /// To provide a prefilled text entry, pass in a [TextEditingController] with
179 180 181 182 183 184 185 186 187
  /// an initial value to the [controller] parameter.
  ///
  /// To provide a hint placeholder text that appears when the text entry is
  /// empty, pass a [String] to the [placeholder] parameter.
  ///
  /// The [maxLines] property can be set to null to remove the restriction on
  /// the number of lines. In this mode, the intrinsic height of the widget will
  /// grow as the number of lines of text grows. By default, it is `1`, meaning
  /// this is a single-line text field and will scroll horizontally when
188
  /// it overflows. [maxLines] must not be zero.
189
  ///
190 191 192
  /// The text cursor is not shown if [showCursor] is false or if [showCursor]
  /// is null (the default) and [readOnly] is true.
  ///
193 194
  /// If specified, the [maxLength] property must be greater than zero.
  ///
195 196
  /// The [selectionHeightStyle] and [selectionWidthStyle] properties allow
  /// changing the shape of the selection highlighting. These properties default
197
  /// to [ui.BoxHeightStyle.tight] and [ui.BoxWidthStyle.tight], respectively.
198
  ///
199
  /// The [autocorrect], [autofocus], [clearButtonMode], [dragStartBehavior],
200 201 202 203
  /// [expands], [obscureText], [prefixMode], [readOnly], [scrollPadding],
  /// [suffixMode], [textAlign], [selectionHeightStyle], [selectionWidthStyle],
  /// [enableSuggestions], and [enableIMEPersonalizedLearning] properties must
  /// not be null.
204
  ///
205 206
  /// {@macro flutter.widgets.editableText.accessibility}
  ///
207 208
  /// See also:
  ///
209 210
  ///  * [minLines], which is the minimum number of lines to occupy when the
  ///    content spans fewer lines.
211
  ///  * [expands], to allow the widget to size itself to its parent's height.
212 213 214
  ///  * [maxLength], which discusses the precise meaning of "number of
  ///    characters" and how it may differ from the intuitive meaning.
  const CupertinoTextField({
215
    super.key,
216 217
    this.controller,
    this.focusNode,
218
    this.undoController,
219
    this.decoration = _kDefaultRoundedBorderDecoration,
220
    this.padding = const EdgeInsets.all(7.0),
221
    this.placeholder,
222
    this.placeholderStyle = const TextStyle(
223 224
      fontWeight: FontWeight.w400,
      color: CupertinoColors.placeholderText,
225
    ),
226 227 228 229 230
    this.prefix,
    this.prefixMode = OverlayVisibilityMode.always,
    this.suffix,
    this.suffixMode = OverlayVisibilityMode.always,
    this.clearButtonMode = OverlayVisibilityMode.never,
231
    TextInputType? keyboardType,
232 233
    this.textInputAction,
    this.textCapitalization = TextCapitalization.none,
xster's avatar
xster committed
234
    this.style,
235
    this.strutStyle,
236
    this.textAlign = TextAlign.start,
237
    this.textAlignVertical,
238
    this.textDirection,
239
    this.readOnly = false,
240 241 242 243 244
    @Deprecated(
      'Use `contextMenuBuilder` instead. '
      'This feature was deprecated after v3.3.0-0.5.pre.',
    )
    this.toolbarOptions,
245
    this.showCursor,
246
    this.autofocus = false,
247
    this.obscuringCharacter = '•',
248 249
    this.obscureText = false,
    this.autocorrect = true,
250 251
    SmartDashesType? smartDashesType,
    SmartQuotesType? smartQuotesType,
252
    this.enableSuggestions = true,
253
    this.maxLines = 1,
254 255
    this.minLines,
    this.expands = false,
256
    this.maxLength,
257
    this.maxLengthEnforcement,
258 259 260
    this.onChanged,
    this.onEditingComplete,
    this.onSubmitted,
261
    this.onTapOutside,
262
    this.inputFormatters,
263
    this.enabled = true,
264 265 266
    this.cursorWidth = 2.0,
    this.cursorHeight,
    this.cursorRadius = const Radius.circular(2.0),
267
    this.cursorOpacityAnimates = true,
268 269 270 271 272 273
    this.cursorColor,
    this.selectionHeightStyle = ui.BoxHeightStyle.tight,
    this.selectionWidthStyle = ui.BoxWidthStyle.tight,
    this.keyboardAppearance,
    this.scrollPadding = const EdgeInsets.all(20.0),
    this.dragStartBehavior = DragStartBehavior.start,
274
    bool? enableInteractiveSelection,
275 276 277 278
    this.selectionControls,
    this.onTap,
    this.scrollController,
    this.scrollPhysics,
279
    this.autofillHints = const <String>[],
280
    this.contentInsertionConfiguration,
281
    this.clipBehavior = Clip.hardEdge,
282
    this.restorationId,
283
    this.scribbleEnabled = true,
284
    this.enableIMEPersonalizedLearning = true,
285
    this.contextMenuBuilder = _defaultContextMenuBuilder,
286
    this.spellCheckConfiguration,
287
    this.magnifierConfiguration,
288
  }) : assert(obscuringCharacter.length == 1),
289 290 291 292 293 294 295 296 297 298 299 300 301 302 303
       smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
       smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
       assert(maxLines == null || maxLines > 0),
       assert(minLines == null || minLines > 0),
       assert(
         (maxLines == null) || (minLines == null) || (maxLines >= minLines),
         "minLines can't be greater than maxLines",
       ),
       assert(
         !expands || (maxLines == null && minLines == null),
         'minLines and maxLines must be null when expands is true.',
       ),
       assert(!obscureText || maxLines == 1, 'Obscured fields cannot be multiline.'),
       assert(maxLength == null || maxLength > 0),
       // Assert the following instead of setting it directly to avoid surprising the user by silently changing the value they set.
304 305
       assert(
         !identical(textInputAction, TextInputAction.newline) ||
306 307
         maxLines == 1 ||
         !identical(keyboardType, TextInputType.text),
308 309
         'Use keyboardType TextInputType.multiline when using TextInputAction.newline on a multiline TextField.',
       ),
310
       keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
311
       enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText);
312 313 314 315 316 317 318 319 320 321 322 323 324

  /// Creates a borderless iOS-style text field.
  ///
  /// To provide a prefilled text entry, pass in a [TextEditingController] with
  /// an initial value to the [controller] parameter.
  ///
  /// To provide a hint placeholder text that appears when the text entry is
  /// empty, pass a [String] to the [placeholder] parameter.
  ///
  /// The [maxLines] property can be set to null to remove the restriction on
  /// the number of lines. In this mode, the intrinsic height of the widget will
  /// grow as the number of lines of text grows. By default, it is `1`, meaning
  /// this is a single-line text field and will scroll horizontally when
325
  /// it overflows. [maxLines] must not be zero.
326 327 328 329 330 331 332 333
  ///
  /// The text cursor is not shown if [showCursor] is false or if [showCursor]
  /// is null (the default) and [readOnly] is true.
  ///
  /// If specified, the [maxLength] property must be greater than zero.
  ///
  /// The [selectionHeightStyle] and [selectionWidthStyle] properties allow
  /// changing the shape of the selection highlighting. These properties default
334
  /// to [ui.BoxHeightStyle.tight] and [ui.BoxWidthStyle.tight] respectively.
335 336 337 338 339 340 341 342 343
  ///
  /// See also:
  ///
  ///  * [minLines], which is the minimum number of lines to occupy when the
  ///    content spans fewer lines.
  ///  * [expands], to allow the widget to size itself to its parent's height.
  ///  * [maxLength], which discusses the precise meaning of "number of
  ///    characters" and how it may differ from the intuitive meaning.
  const CupertinoTextField.borderless({
344
    super.key,
345 346
    this.controller,
    this.focusNode,
347
    this.undoController,
348
    this.decoration,
349
    this.padding = const EdgeInsets.all(7.0),
350 351 352 353 354 355 356 357 358 359 360 361 362 363
    this.placeholder,
    this.placeholderStyle = _kDefaultPlaceholderStyle,
    this.prefix,
    this.prefixMode = OverlayVisibilityMode.always,
    this.suffix,
    this.suffixMode = OverlayVisibilityMode.always,
    this.clearButtonMode = OverlayVisibilityMode.never,
    TextInputType? keyboardType,
    this.textInputAction,
    this.textCapitalization = TextCapitalization.none,
    this.style,
    this.strutStyle,
    this.textAlign = TextAlign.start,
    this.textAlignVertical,
364
    this.textDirection,
365
    this.readOnly = false,
366 367 368 369 370
    @Deprecated(
      'Use `contextMenuBuilder` instead. '
      'This feature was deprecated after v3.3.0-0.5.pre.',
    )
    this.toolbarOptions,
371 372 373 374 375 376 377 378 379 380 381 382
    this.showCursor,
    this.autofocus = false,
    this.obscuringCharacter = '•',
    this.obscureText = false,
    this.autocorrect = true,
    SmartDashesType? smartDashesType,
    SmartQuotesType? smartQuotesType,
    this.enableSuggestions = true,
    this.maxLines = 1,
    this.minLines,
    this.expands = false,
    this.maxLength,
383
    this.maxLengthEnforcement,
384 385 386
    this.onChanged,
    this.onEditingComplete,
    this.onSubmitted,
387
    this.onTapOutside,
388
    this.inputFormatters,
389
    this.enabled = true,
390
    this.cursorWidth = 2.0,
391
    this.cursorHeight,
392
    this.cursorRadius = const Radius.circular(2.0),
393
    this.cursorOpacityAnimates = true,
394
    this.cursorColor,
395 396
    this.selectionHeightStyle = ui.BoxHeightStyle.tight,
    this.selectionWidthStyle = ui.BoxWidthStyle.tight,
397 398
    this.keyboardAppearance,
    this.scrollPadding = const EdgeInsets.all(20.0),
399
    this.dragStartBehavior = DragStartBehavior.start,
400
    bool? enableInteractiveSelection,
401
    this.selectionControls,
402
    this.onTap,
403
    this.scrollController,
404
    this.scrollPhysics,
405
    this.autofillHints = const <String>[],
406
    this.contentInsertionConfiguration,
407
    this.clipBehavior = Clip.hardEdge,
408
    this.restorationId,
409
    this.scribbleEnabled = true,
410
    this.enableIMEPersonalizedLearning = true,
411
    this.contextMenuBuilder = _defaultContextMenuBuilder,
412
    this.spellCheckConfiguration,
413
    this.magnifierConfiguration,
414
  }) : assert(obscuringCharacter.length == 1),
415 416
       smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
       smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
417
       assert(maxLines == null || maxLines > 0),
418 419 420
       assert(minLines == null || minLines > 0),
       assert(
         (maxLines == null) || (minLines == null) || (maxLines >= minLines),
421
         "minLines can't be greater than maxLines",
422 423 424 425 426
       ),
       assert(
         !expands || (maxLines == null && minLines == null),
         'minLines and maxLines must be null when expands is true.',
       ),
427
       assert(!obscureText || maxLines == 1, 'Obscured fields cannot be multiline.'),
428
       assert(maxLength == null || maxLength > 0),
429
       // Assert the following instead of setting it directly to avoid surprising the user by silently changing the value they set.
430 431
       assert(
         !identical(textInputAction, TextInputAction.newline) ||
432 433
         maxLines == 1 ||
         !identical(keyboardType, TextInputType.text),
434 435
         'Use keyboardType TextInputType.multiline when using TextInputAction.newline on a multiline TextField.',
       ),
436
       keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
437
       enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText);
438 439 440 441

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

444
  /// {@macro flutter.widgets.Focus.focusNode}
445
  final FocusNode? focusNode;
446 447 448 449 450

  /// Controls the [BoxDecoration] of the box behind the text input.
  ///
  /// Defaults to having a rounded rectangle grey border and can be null to have
  /// no box decoration.
451
  final BoxDecoration? decoration;
452 453 454 455 456 457 458 459 460 461 462 463 464 465

  /// Padding around the text entry area between the [prefix] and [suffix]
  /// or the clear button when [clearButtonMode] is not never.
  ///
  /// Defaults to a padding of 6 pixels on all sides and can be null.
  final EdgeInsetsGeometry padding;

  /// A lighter colored placeholder hint that appears on the first line of the
  /// text field when the text entry is empty.
  ///
  /// Defaults to having no placeholder text.
  ///
  /// The text style of the placeholder text matches that of the text field's
  /// main text entry except a lighter font weight and a grey font color.
466
  final String? placeholder;
467

468 469 470 471 472 473 474 475 476
  /// The style to use for the placeholder text.
  ///
  /// The [placeholderStyle] is merged with the [style] [TextStyle] when applied
  /// to the [placeholder] text. To avoid merging with [style], specify
  /// [TextStyle.inherit] as false.
  ///
  /// Defaults to the [style] property with w300 font weight and grey color.
  ///
  /// If specifically set to null, placeholder's style will be the same as [style].
477
  final TextStyle? placeholderStyle;
478

479
  /// An optional [Widget] to display before the text.
480
  final Widget? prefix;
481 482 483 484

  /// Controls the visibility of the [prefix] widget based on the state of
  /// text entry when the [prefix] argument is not null.
  ///
485
  /// Defaults to [OverlayVisibilityMode.always].
486 487 488 489 490
  ///
  /// Has no effect when [prefix] is null.
  final OverlayVisibilityMode prefixMode;

  /// An optional [Widget] to display after the text.
491
  final Widget? suffix;
492 493 494 495

  /// Controls the visibility of the [suffix] widget based on the state of
  /// text entry when the [suffix] argument is not null.
  ///
496
  /// Defaults to [OverlayVisibilityMode.always].
497 498 499 500 501 502 503 504 505 506 507
  ///
  /// Has no effect when [suffix] is null.
  final OverlayVisibilityMode suffixMode;

  /// Show an iOS-style clear button to clear the current text entry.
  ///
  /// Can be made to appear depending on various text states of the
  /// [TextEditingController].
  ///
  /// Will only appear if no [suffix] widget is appearing.
  ///
508
  /// Defaults to [OverlayVisibilityMode.never].
509 510 511 512 513 514 515 516 517
  final OverlayVisibilityMode clearButtonMode;

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

  /// The type of action button to use for the keyboard.
  ///
  /// Defaults to [TextInputAction.newline] if [keyboardType] is
  /// [TextInputType.multiline] and [TextInputAction.done] otherwise.
518
  final TextInputAction? textInputAction;
519 520 521 522 523 524 525 526

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

  /// The style to use for the text being edited.
  ///
  /// Also serves as a base for the [placeholder] text's style.
  ///
xster's avatar
xster committed
527
  /// Defaults to the standard iOS font style from [CupertinoTheme] if null.
528
  final TextStyle? style;
529

530
  /// {@macro flutter.widgets.editableText.strutStyle}
531
  final StrutStyle? strutStyle;
532

533 534 535
  /// {@macro flutter.widgets.editableText.textAlign}
  final TextAlign textAlign;

536 537 538 539 540
  /// 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.
541 542 543 544 545
  @Deprecated(
    'Use `contextMenuBuilder` instead. '
    'This feature was deprecated after v3.3.0-0.5.pre.',
  )
  final ToolbarOptions? toolbarOptions;
546

547
  /// {@macro flutter.material.InputDecorator.textAlignVertical}
548
  final TextAlignVertical? textAlignVertical;
549

550 551 552
  /// {@macro flutter.widgets.editableText.textDirection}
  final TextDirection? textDirection;

553 554 555 556
  /// {@macro flutter.widgets.editableText.readOnly}
  final bool readOnly;

  /// {@macro flutter.widgets.editableText.showCursor}
557
  final bool? showCursor;
558

559 560 561
  /// {@macro flutter.widgets.editableText.autofocus}
  final bool autofocus;

562 563 564
  /// {@macro flutter.widgets.editableText.obscuringCharacter}
  final String obscuringCharacter;

565 566 567 568 569 570
  /// {@macro flutter.widgets.editableText.obscureText}
  final bool obscureText;

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

571
  /// {@macro flutter.services.TextInputConfiguration.smartDashesType}
572 573
  final SmartDashesType smartDashesType;

574
  /// {@macro flutter.services.TextInputConfiguration.smartQuotesType}
575 576
  final SmartQuotesType smartQuotesType;

577
  /// {@macro flutter.services.TextInputConfiguration.enableSuggestions}
578 579
  final bool enableSuggestions;

580
  /// {@macro flutter.widgets.editableText.maxLines}
581 582
  ///  * [expands], which determines whether the field should fill the height of
  ///    its parent.
583
  final int? maxLines;
584

585
  /// {@macro flutter.widgets.editableText.minLines}
586 587
  ///  * [expands], which determines whether the field should fill the height of
  ///    its parent.
588
  final int? minLines;
589 590 591 592

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

593 594
  /// The maximum number of characters (Unicode grapheme clusters) to allow in
  /// the text field.
595
  ///
596 597 598 599 600 601 602
  /// After [maxLength] characters have been input, additional input
  /// is ignored, unless [maxLengthEnforcement] is set to
  /// [MaxLengthEnforcement.none].
  ///
  /// The TextField enforces the length with a
  /// [LengthLimitingTextInputFormatter], which is evaluated after the supplied
  /// [inputFormatters], if any.
603 604 605 606 607 608 609
  ///
  /// This value must be either null or greater than zero. If set to null
  /// (the default), there is no limit to the number of characters allowed.
  ///
  /// Whitespace characters (e.g. newline, space, tab) are included in the
  /// character count.
  ///
610
  /// {@macro flutter.services.lengthLimitingTextInputFormatter.maxLength}
611
  final int? maxLength;
612

613 614 615 616 617 618 619 620 621 622
  /// Determines how the [maxLength] limit should be enforced.
  ///
  /// If [MaxLengthEnforcement.none] is set, additional input beyond [maxLength]
  /// will not be enforced by the limit.
  ///
  /// {@macro flutter.services.textFormatter.effectiveMaxLengthEnforcement}
  ///
  /// {@macro flutter.services.textFormatter.maxLengthEnforcement}
  final MaxLengthEnforcement? maxLengthEnforcement;

623
  /// {@macro flutter.widgets.editableText.onChanged}
624
  final ValueChanged<String>? onChanged;
625 626

  /// {@macro flutter.widgets.editableText.onEditingComplete}
627
  final VoidCallback? onEditingComplete;
628 629

  /// {@macro flutter.widgets.editableText.onSubmitted}
630 631 632
  ///
  /// See also:
  ///
633 634 635
  ///  * [TextInputAction.next] and [TextInputAction.previous], which
  ///    automatically shift the focus to the next/previous focusable item when
  ///    the user is done editing.
636
  final ValueChanged<String>? onSubmitted;
637

638 639 640
  /// {@macro flutter.widgets.editableText.onTapOutside}
  final TapRegionCallback? onTapOutside;

641
  /// {@macro flutter.widgets.editableText.inputFormatters}
642
  final List<TextInputFormatter>? inputFormatters;
643 644 645 646 647 648

  /// Disables the text field when false.
  ///
  /// Text fields in disabled states have a light grey background and don't
  /// respond to touch events including the [prefix], [suffix] and the clear
  /// button.
649 650 651
  ///
  /// Defaults to true.
  final bool enabled;
652 653 654 655

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

656
  /// {@macro flutter.widgets.editableText.cursorHeight}
657
  final double? cursorHeight;
658

659 660 661
  /// {@macro flutter.widgets.editableText.cursorRadius}
  final Radius cursorRadius;

662 663 664
  /// {@macro flutter.widgets.editableText.cursorOpacityAnimates}
  final bool cursorOpacityAnimates;

665 666
  /// The color to use when painting the cursor.
  ///
667 668
  /// Defaults to the [DefaultSelectionStyle.cursorColor]. If that color is
  /// null, it uses the [CupertinoThemeData.primaryColor] of the ambient theme,
669 670
  /// which itself defaults to [CupertinoColors.activeBlue] in the light theme
  /// and [CupertinoColors.activeOrange] in the dark theme.
671
  final Color? cursorColor;
672

673 674 675 676 677 678 679 680 681 682
  /// 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;

683 684 685 686 687
  /// The appearance of the keyboard.
  ///
  /// This setting is only honored on iOS devices.
  ///
  /// If null, defaults to [Brightness.light].
688
  final Brightness? keyboardAppearance;
689 690 691 692

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

693 694 695
  /// {@macro flutter.widgets.editableText.enableInteractiveSelection}
  final bool enableInteractiveSelection;

696 697 698
  /// {@macro flutter.widgets.editableText.selectionControls}
  final TextSelectionControls? selectionControls;

699 700 701
  /// {@macro flutter.widgets.scrollable.dragStartBehavior}
  final DragStartBehavior dragStartBehavior;

702
  /// {@macro flutter.widgets.editableText.scrollController}
703
  final ScrollController? scrollController;
704

Dan Field's avatar
Dan Field committed
705
  /// {@macro flutter.widgets.editableText.scrollPhysics}
706
  final ScrollPhysics? scrollPhysics;
707

708
  /// {@macro flutter.widgets.editableText.selectionEnabled}
709
  bool get selectionEnabled => enableInteractiveSelection;
710

711
  /// {@macro flutter.material.textfield.onTap}
712
  final GestureTapCallback? onTap;
713

714
  /// {@macro flutter.widgets.editableText.autofillHints}
715
  /// {@macro flutter.services.AutofillConfiguration.autofillHints}
716
  final Iterable<String>? autofillHints;
717

718 719 720 721 722
  /// {@macro flutter.material.Material.clipBehavior}
  ///
  /// Defaults to [Clip.hardEdge].
  final Clip clipBehavior;

723
  /// {@macro flutter.material.textfield.restorationId}
724
  final String? restorationId;
725

726 727 728
  /// {@macro flutter.widgets.editableText.scribbleEnabled}
  final bool scribbleEnabled;

729 730 731
  /// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
  final bool enableIMEPersonalizedLearning;

732 733 734
  /// {@macro flutter.widgets.editableText.contentInsertionConfiguration}
  final ContentInsertionConfiguration? contentInsertionConfiguration;

735 736 737 738 739 740 741 742 743 744 745 746 747 748 749
  /// {@macro flutter.widgets.EditableText.contextMenuBuilder}
  ///
  /// If not provided, will build a default menu based on the platform.
  ///
  /// See also:
  ///
  ///  * [CupertinoAdaptiveTextSelectionToolbar], which is built by default.
  final EditableTextContextMenuBuilder? contextMenuBuilder;

  static Widget _defaultContextMenuBuilder(BuildContext context, EditableTextState editableTextState) {
    return CupertinoAdaptiveTextSelectionToolbar.editableText(
      editableTextState: editableTextState,
    );
  }

750
  /// {@macro flutter.widgets.magnifier.TextMagnifierConfiguration.intro}
751 752 753
  ///
  /// {@macro flutter.widgets.magnifier.intro}
  ///
754
  /// {@macro flutter.widgets.magnifier.TextMagnifierConfiguration.details}
755 756
  ///
  /// By default, builds a [CupertinoTextMagnifier] on iOS and Android nothing on all other
757
  /// platforms. If it is desired to suppress the magnifier, consider passing
758 759
  /// [TextMagnifierConfiguration.disabled].
  ///
760 761 762 763 764
  /// {@tool dartpad}
  /// This sample demonstrates how to customize the magnifier that this text field uses.
  ///
  /// ** See code in examples/api/lib/widgets/text_magnifier/text_magnifier.0.dart **
  /// {@end-tool}
765 766
  final TextMagnifierConfiguration? magnifierConfiguration;

767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786
  /// {@macro flutter.widgets.EditableText.spellCheckConfiguration}
  ///
  /// If [SpellCheckConfiguration.misspelledTextStyle] is not specified in this
  /// configuration, then [cupertinoMisspelledTextStyle] is used by default.
  final SpellCheckConfiguration? spellCheckConfiguration;

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

787 788 789 790 791 792
  /// The color of the selection highlight when the spell check menu is visible.
  ///
  /// Eyeballed from a screenshot taken on an iPhone 11 running iOS 16.2.
  @visibleForTesting
  static const Color kMisspelledSelectionColor = Color(0x62ff9699);

793 794 795 796
  /// Default builder for the spell check suggestions toolbar in the Cupertino
  /// style.
  ///
  /// See also:
797 798
  ///  * [spellCheckConfiguration], where this is typically specified for
  ///    [CupertinoTextField].
799
  ///  * [SpellCheckConfiguration.spellCheckSuggestionsToolbarBuilder], the
800 801 802
  ///    parameter for which this is the default value for [CupertinoTextField].
  ///  * [TextField.defaultSpellCheckSuggestionsToolbarBuilder], which is like
  ///    this but specifies the default for [CupertinoTextField].
803 804 805 806 807
  @visibleForTesting
  static Widget defaultSpellCheckSuggestionsToolbarBuilder(
    BuildContext context,
    EditableTextState editableTextState,
  ) {
808 809
    return CupertinoSpellCheckSuggestionsToolbar.editableText(
      editableTextState: editableTextState,
810 811 812
    );
  }

813 814 815
  /// {@macro flutter.widgets.undoHistory.controller}
  final UndoHistoryController? undoController;

816
  @override
817
  State<CupertinoTextField> createState() => _CupertinoTextFieldState();
818 819 820 821 822 823

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DiagnosticsProperty<TextEditingController>('controller', controller, defaultValue: null));
    properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode, defaultValue: null));
824
    properties.add(DiagnosticsProperty<UndoHistoryController>('undoController', undoController, defaultValue: null));
825 826 827
    properties.add(DiagnosticsProperty<BoxDecoration>('decoration', decoration));
    properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding));
    properties.add(StringProperty('placeholder', placeholder));
828
    properties.add(DiagnosticsProperty<TextStyle>('placeholderStyle', placeholderStyle));
829 830 831 832 833 834
    properties.add(DiagnosticsProperty<OverlayVisibilityMode>('prefix', prefix == null ? null : prefixMode));
    properties.add(DiagnosticsProperty<OverlayVisibilityMode>('suffix', suffix == null ? null : suffixMode));
    properties.add(DiagnosticsProperty<OverlayVisibilityMode>('clearButtonMode', clearButtonMode));
    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));
835
    properties.add(DiagnosticsProperty<String>('obscuringCharacter', obscuringCharacter, defaultValue: '•'));
836
    properties.add(DiagnosticsProperty<bool>('obscureText', obscureText, defaultValue: false));
837
    properties.add(DiagnosticsProperty<bool>('autocorrect', autocorrect, defaultValue: true));
838 839
    properties.add(EnumProperty<SmartDashesType>('smartDashesType', smartDashesType, defaultValue: obscureText ? SmartDashesType.disabled : SmartDashesType.enabled));
    properties.add(EnumProperty<SmartQuotesType>('smartQuotesType', smartQuotesType, defaultValue: obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled));
840
    properties.add(DiagnosticsProperty<bool>('enableSuggestions', enableSuggestions, defaultValue: true));
841
    properties.add(IntProperty('maxLines', maxLines, defaultValue: 1));
842 843
    properties.add(IntProperty('minLines', minLines, defaultValue: null));
    properties.add(DiagnosticsProperty<bool>('expands', expands, defaultValue: false));
844
    properties.add(IntProperty('maxLength', maxLength, defaultValue: null));
845
    properties.add(EnumProperty<MaxLengthEnforcement>('maxLengthEnforcement', maxLengthEnforcement, defaultValue: null));
846 847 848
    properties.add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0));
    properties.add(DoubleProperty('cursorHeight', cursorHeight, defaultValue: null));
    properties.add(DiagnosticsProperty<Radius>('cursorRadius', cursorRadius, defaultValue: null));
849
    properties.add(DiagnosticsProperty<bool>('cursorOpacityAnimates', cursorOpacityAnimates, defaultValue: true));
850
    properties.add(createCupertinoColorProperty('cursorColor', cursorColor, defaultValue: null));
851
    properties.add(FlagProperty('selectionEnabled', value: selectionEnabled, defaultValue: true, ifFalse: 'selection disabled'));
852
    properties.add(DiagnosticsProperty<TextSelectionControls>('selectionControls', selectionControls, defaultValue: null));
853
    properties.add(DiagnosticsProperty<ScrollController>('scrollController', scrollController, defaultValue: null));
854
    properties.add(DiagnosticsProperty<ScrollPhysics>('scrollPhysics', scrollPhysics, defaultValue: null));
855 856
    properties.add(EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: TextAlign.start));
    properties.add(DiagnosticsProperty<TextAlignVertical>('textAlignVertical', textAlignVertical, defaultValue: null));
857
    properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
858
    properties.add(DiagnosticsProperty<Clip>('clipBehavior', clipBehavior, defaultValue: Clip.hardEdge));
859
    properties.add(DiagnosticsProperty<bool>('scribbleEnabled', scribbleEnabled, defaultValue: true));
860
    properties.add(DiagnosticsProperty<bool>('enableIMEPersonalizedLearning', enableIMEPersonalizedLearning, defaultValue: true));
861
    properties.add(DiagnosticsProperty<SpellCheckConfiguration>('spellCheckConfiguration', spellCheckConfiguration, defaultValue: null));
862
    properties.add(DiagnosticsProperty<List<String>>('contentCommitMimeTypes', contentInsertionConfiguration?.allowedMimeTypes ?? const <String>[], defaultValue: contentInsertionConfiguration == null ? const <String>[] : kDefaultContentInsertionMimeTypes));
863
  }
864 865 866 867 868

  static final TextMagnifierConfiguration _iosMagnifierConfiguration = TextMagnifierConfiguration(
    magnifierBuilder: (
    BuildContext context,
    MagnifierController controller,
869
    ValueNotifier<MagnifierInfo> magnifierInfo
870 871 872 873 874 875
  ) {
    switch (defaultTargetPlatform) {
      case TargetPlatform.android:
      case TargetPlatform.iOS:
        return CupertinoTextMagnifier(
        controller: controller,
876
        magnifierInfo: magnifierInfo,
877 878 879 880 881 882 883 884
      );
      case TargetPlatform.fuchsia:
      case TargetPlatform.linux:
      case TargetPlatform.macOS:
      case TargetPlatform.windows:
        return null;
    }
  });
885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905

  /// Returns a new [SpellCheckConfiguration] where the given configuration has
  /// had any missing values replaced with their defaults for the iOS platform.
  static SpellCheckConfiguration inferIOSSpellCheckConfiguration(
    SpellCheckConfiguration? configuration,
  ) {
    if (configuration == null
      || configuration == const SpellCheckConfiguration.disabled()) {
      return const SpellCheckConfiguration.disabled();
    }

    return configuration.copyWith(
      misspelledTextStyle: configuration.misspelledTextStyle
        ?? CupertinoTextField.cupertinoMisspelledTextStyle,
      misspelledSelectionColor: configuration.misspelledSelectionColor
        ?? CupertinoTextField.kMisspelledSelectionColor,
      spellCheckSuggestionsToolbarBuilder:
        configuration.spellCheckSuggestionsToolbarBuilder
          ?? CupertinoTextField.defaultSpellCheckSuggestionsToolbarBuilder,
    );
  }
906 907
}

908
class _CupertinoTextFieldState extends State<CupertinoTextField> with RestorationMixin, AutomaticKeepAliveClientMixin<CupertinoTextField> implements TextSelectionGestureDetectorBuilderDelegate, AutofillClient {
909
  final GlobalKey _clearGlobalKey = GlobalKey();
910

911 912
  RestorableTextEditingController? _controller;
  TextEditingController get _effectiveController => widget.controller ?? _controller!.value;
913

914
  FocusNode? _focusNode;
915 916
  FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode());

917
  MaxLengthEnforcement get _effectiveMaxLengthEnforcement => widget.maxLengthEnforcement
918
    ?? LengthLimitingTextInputFormatter.getDefaultMaxLengthEnforcement();
919

920 921
  bool _showSelectionHandles = false;

922
  late _CupertinoTextFieldSelectionGestureDetectorBuilder _selectionGestureDetectorBuilder;
923 924 925 926 927 928 929 930 931 932 933 934

  // API for TextSelectionGestureDetectorBuilderDelegate.
  @override
  bool get forcePressEnabled => true;

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

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

935 936 937
  @override
  void initState() {
    super.initState();
938 939 940
    _selectionGestureDetectorBuilder = _CupertinoTextFieldSelectionGestureDetectorBuilder(
      state: this,
    );
941
    if (widget.controller == null) {
942
      _createLocalController();
943
    }
944
    _effectiveFocusNode.canRequestFocus = widget.enabled;
945
    _effectiveFocusNode.addListener(_handleFocusChanged);
946 947 948 949 950 951
  }

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

    if (widget.focusNode != oldWidget.focusNode) {
      (oldWidget.focusNode ?? _focusNode)?.removeListener(_handleFocusChanged);
      (widget.focusNode ?? _focusNode)?.addListener(_handleFocusChanged);
    }
963
    _effectiveFocusNode.canRequestFocus = widget.enabled;
964 965
  }

966
  @override
967
  void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
968 969 970 971 972 973 974
    if (_controller != null) {
      _registerController();
    }
  }

  void _registerController() {
    assert(_controller != null);
975 976
    registerForRestoration(_controller!, 'controller');
    _controller!.value.addListener(updateKeepAlive);
977 978
  }

979
  void _createLocalController([TextEditingValue? value]) {
980 981 982 983 984 985 986 987 988 989
    assert(_controller == null);
    _controller = value == null
        ? RestorableTextEditingController()
        : RestorableTextEditingController.fromValue(value);
    if (!restorePending) {
      _registerController();
    }
  }

  @override
990
  String? get restorationId => widget.restorationId;
991

992 993
  @override
  void dispose() {
994
    _effectiveFocusNode.removeListener(_handleFocusChanged);
995
    _focusNode?.dispose();
996
    _controller?.dispose();
997 998 999
    super.dispose();
  }

1000
  EditableTextState get _editableText => editableTextKey.currentState!;
1001

1002
  void _requestKeyboard() {
1003
    _editableText.requestKeyboard();
1004 1005
  }

1006 1007 1008 1009 1010 1011 1012
  void _handleFocusChanged() {
    setState(() {
      // Rebuild the widget on focus change to show/hide the text selection
      // highlight.
    });
  }

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

    // On iOS, we don't show handles when the selection is collapsed.
1021
    if (_effectiveController.selection.isCollapsed) {
1022
      return false;
1023
    }
1024

1025
    if (cause == SelectionChangedCause.keyboard) {
1026
      return false;
1027
    }
1028

1029
    if (cause == SelectionChangedCause.scribble) {
1030
      return true;
1031
    }
1032

1033
    if (_effectiveController.text.isNotEmpty) {
1034
      return true;
1035
    }
1036 1037

    return false;
1038 1039
  }

1040
  void _handleSelectionChanged(TextSelection selection, SelectionChangedCause? cause) {
1041 1042 1043 1044 1045
    final bool willShowSelectionHandles = _shouldShowSelectionHandles(cause);
    if (willShowSelectionHandles != _showSelectionHandles) {
      setState(() {
        _showSelectionHandles = willShowSelectionHandles;
      });
1046
    }
1047 1048 1049 1050 1051 1052 1053 1054

    switch (defaultTargetPlatform) {
      case TargetPlatform.iOS:
      case TargetPlatform.macOS:
      case TargetPlatform.linux:
      case TargetPlatform.windows:
      case TargetPlatform.fuchsia:
      case TargetPlatform.android:
1055
        if (cause == SelectionChangedCause.longPress) {
1056 1057
          _editableText.bringIntoView(selection.extent);
        }
1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070
    }

    switch (defaultTargetPlatform) {
      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();
        }
1071
    }
1072 1073
  }

1074
  @override
1075
  bool get wantKeepAlive => _controller?.value.text.isNotEmpty ?? false;
1076

1077
  static bool _shouldShowAttachment({
1078 1079
    required OverlayVisibilityMode attachment,
    required bool hasText,
1080
  }) {
1081 1082 1083 1084 1085 1086
    return switch (attachment) {
      OverlayVisibilityMode.never => false,
      OverlayVisibilityMode.always => true,
      OverlayVisibilityMode.editing => hasText,
      OverlayVisibilityMode.notEditing => !hasText,
    };
1087 1088
  }

1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101
  // True if any surrounding decoration widgets will be shown.
  bool get _hasDecoration {
    return widget.placeholder != null ||
      widget.clearButtonMode != OverlayVisibilityMode.never ||
      widget.prefix != null ||
      widget.suffix != null;
  }

  // Provide default behavior if widget.textAlignVertical is not set.
  // CupertinoTextField has top alignment by default, unless it has decoration
  // like a prefix or suffix, in which case it's aligned to the center.
  TextAlignVertical get _textAlignVertical {
    if (widget.textAlignVertical != null) {
1102
      return widget.textAlignVertical!;
1103 1104 1105 1106
    }
    return _hasDecoration ? TextAlignVertical.center : TextAlignVertical.top;
  }

1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132
  void _onClearButtonTapped() {
    final bool hadText = _effectiveController.text.isNotEmpty;
    _effectiveController.clear();
    if (hadText) {
      // Tapping the clear button is also considered a "user initiated" change
      // (instead of a programmatical one), so call `onChanged` if the text
      // changed as a result.
      widget.onChanged?.call(_effectiveController.text);
    }
  }

  Widget _buildClearButton() {
    return GestureDetector(
      key: _clearGlobalKey,
      onTap: widget.enabled ? _onClearButtonTapped : null,
      child: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 6.0),
        child: Icon(
          CupertinoIcons.clear_thick_circled,
          size: 18.0,
          color: CupertinoDynamicColor.resolve(_kClearButtonColor, context),
        ),
      ),
    );
  }

1133
  Widget _addTextDependentAttachments(Widget editableText, TextStyle textStyle, TextStyle placeholderStyle) {
1134 1135
    // If there are no surrounding widgets, just return the core editable text
    // part.
1136
    if (!_hasDecoration) {
1137 1138 1139 1140 1141 1142 1143
      return editableText;
    }

    // Otherwise, listen to the current state of the text entry.
    return ValueListenableBuilder<TextEditingValue>(
      valueListenable: _effectiveController,
      child: editableText,
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
      builder: (BuildContext context, TextEditingValue text, Widget? child) {
        final bool hasText = text.text.isNotEmpty;
        final String? placeholderText = widget.placeholder;
        final Widget? placeholder = placeholderText == null
          ? null
          // Make the placeholder invisible when hasText is true.
          : Visibility(
              maintainAnimation: true,
              maintainSize: true,
              maintainState: true,
              visible: !hasText,
              child: SizedBox(
                width: double.infinity,
                child: Padding(
                  padding: widget.padding,
                  child: Text(
                    placeholderText,
                    // This is to make sure the text field is always tall enough
                    // to accommodate the first line of the placeholder, so the
                    // text does not shrink vertically as you type (however in
                    // rare circumstances, the height may still change when
                    // there's no placeholder text).
                    maxLines: hasText ? 1 : widget.maxLines,
                    overflow: placeholderStyle.overflow,
                    style: placeholderStyle,
                    textAlign: widget.textAlign,
                  ),
                ),
              ),
          );

        final Widget? prefixWidget = _shouldShowAttachment(attachment: widget.prefixMode, hasText: hasText) ? widget.prefix : null;

        // Show user specified suffix if applicable and fall back to clear button.
        final bool showUserSuffix = _shouldShowAttachment(attachment: widget.suffixMode, hasText: hasText);
        final bool showClearButton = _shouldShowAttachment(attachment: widget.clearButtonMode, hasText: hasText);
        final Widget? suffixWidget = switch ((showUserSuffix, showClearButton)) {
          (false, false) => null,
          (true, false) => widget.suffix,
          (true, true) => widget.suffix ?? _buildClearButton(),
          (false, true) => _buildClearButton(),
        };
1186 1187 1188
        return Row(children: <Widget>[
          // Insert a prefix at the front if the prefix visibility mode matches
          // the current text state.
1189
          if (prefixWidget != null) prefixWidget,
1190 1191 1192 1193
          // In the middle part, stack the placeholder on top of the main EditableText
          // if needed.
          Expanded(
            child: Stack(
1194 1195 1196 1197 1198 1199
              // Ideally this should be baseline aligned. However that comes at
              // the cost of the ability to compute the intrinsic dimensions of
              // this widget.
              // See also https://github.com/flutter/flutter/issues/13715.
              alignment: AlignmentDirectional.center,
              textDirection: widget.textDirection,
1200
              children: <Widget>[
1201 1202
                if (placeholder != null) placeholder,
                editableText,
1203
              ],
1204
            ),
1205
          ),
1206
          if (suffixWidget != null) suffixWidget
1207
        ]);
1208 1209 1210
      },
    );
  }
1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232
  // 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.placeholder,
        )
      : AutofillConfiguration.disabled;

    return _editableText.textInputConfiguration.copyWith(autofillConfiguration: autofillConfiguration);
  }
  // AutofillClient implementation end.
1233 1234 1235 1236 1237 1238

  @override
  Widget build(BuildContext context) {
    super.build(context); // See AutomaticKeepAliveClientMixin.
    assert(debugCheckHasDirectionality(context));
    final TextEditingController controller = _effectiveController;
1239 1240

    TextSelectionControls? textSelectionControls = widget.selectionControls;
1241
    VoidCallback? handleDidGainAccessibilityFocus;
1242 1243 1244 1245 1246
    switch (defaultTargetPlatform) {
      case TargetPlatform.iOS:
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
      case TargetPlatform.linux:
1247
        textSelectionControls ??= cupertinoTextSelectionHandleControls;
1248 1249

      case TargetPlatform.macOS:
1250
      case TargetPlatform.windows:
1251
        textSelectionControls ??= cupertinoDesktopTextSelectionHandleControls;
1252
        handleDidGainAccessibilityFocus = () {
1253
          // Automatically activate the TextField when it receives accessibility focus.
1254 1255
          if (!_effectiveFocusNode.hasFocus && _effectiveFocusNode.canRequestFocus) {
            _effectiveFocusNode.requestFocus();
1256 1257
          }
        };
1258 1259
    }

1260
    final bool enabled = widget.enabled;
1261
    final Offset cursorOffset = Offset(_iOSHorizontalCursorOffsetPixels / MediaQuery.devicePixelRatioOf(context), 0);
1262 1263
    final List<TextInputFormatter> formatters = <TextInputFormatter>[
      ...?widget.inputFormatters,
1264
      if (widget.maxLength != null)
1265 1266 1267 1268
        LengthLimitingTextInputFormatter(
          widget.maxLength,
          maxLengthEnforcement: _effectiveMaxLengthEnforcement,
        ),
1269
    ];
1270
    final CupertinoThemeData themeData = CupertinoTheme.of(context);
1271

1272
    final TextStyle? resolvedStyle = widget.style?.copyWith(
1273 1274
      color: CupertinoDynamicColor.maybeResolve(widget.style?.color, context),
      backgroundColor: CupertinoDynamicColor.maybeResolve(widget.style?.backgroundColor, context),
1275 1276 1277 1278
    );

    final TextStyle textStyle = themeData.textTheme.textStyle.merge(resolvedStyle);

1279
    final TextStyle? resolvedPlaceholderStyle = widget.placeholderStyle?.copyWith(
1280 1281
      color: CupertinoDynamicColor.maybeResolve(widget.placeholderStyle?.color, context),
      backgroundColor: CupertinoDynamicColor.maybeResolve(widget.placeholderStyle?.backgroundColor, context),
1282 1283 1284 1285
    );

    final TextStyle placeholderStyle = textStyle.merge(resolvedPlaceholderStyle);

1286
    final Brightness keyboardAppearance = widget.keyboardAppearance ?? CupertinoTheme.brightnessOf(context);
1287 1288 1289 1290 1291
    final Color cursorColor = CupertinoDynamicColor.maybeResolve(
      widget.cursorColor ?? DefaultSelectionStyle.of(context).cursorColor,
      context,
    ) ?? themeData.primaryColor;

1292
    final Color disabledColor = CupertinoDynamicColor.resolve(_kDisabledBackground, context);
1293

1294
    final Color? decorationColor = CupertinoDynamicColor.maybeResolve(widget.decoration?.color, context);
1295

1296
    final BoxBorder? border = widget.decoration?.border;
1297
    Border? resolvedBorder = border as Border?;
1298 1299 1300 1301 1302 1303
    if (border is Border) {
      BorderSide resolveBorderSide(BorderSide side) {
        return side == BorderSide.none
          ? side
          : side.copyWith(color: CupertinoDynamicColor.resolve(side.color, context));
      }
1304
      resolvedBorder = border.runtimeType != Border
1305 1306 1307 1308 1309 1310 1311 1312
        ? border
        : Border(
          top: resolveBorderSide(border.top),
          left: resolveBorderSide(border.left),
          bottom: resolveBorderSide(border.bottom),
          right: resolveBorderSide(border.right),
        );
    }
1313

1314
    final BoxDecoration? effectiveDecoration = widget.decoration?.copyWith(
1315
      border: resolvedBorder,
1316
      color: enabled ? decorationColor : disabledColor,
1317
    );
1318

1319 1320 1321 1322
    final Color selectionColor = CupertinoDynamicColor.maybeResolve(
      DefaultSelectionStyle.of(context).selectionColor,
      context,
    ) ?? CupertinoTheme.of(context).primaryColor.withOpacity(0.2);
1323

1324 1325 1326 1327
    // Set configuration as disabled if not otherwise specified. If specified,
    // ensure that configuration uses Cupertino text style for misspelled words
    // unless a custom style is specified.
    final SpellCheckConfiguration spellCheckConfiguration =
1328 1329 1330
        CupertinoTextField.inferIOSSpellCheckConfiguration(
          widget.spellCheckConfiguration,
        );
1331

1332
    final Widget paddedEditable = Padding(
1333 1334
      padding: widget.padding,
      child: RepaintBoundary(
1335 1336 1337 1338 1339
        child: UnmanagedRestorationScope(
          bucket: bucket,
          child: EditableText(
            key: editableTextKey,
            controller: controller,
1340
            undoController: widget.undoController,
1341
            readOnly: widget.readOnly || !enabled,
1342 1343 1344
            toolbarOptions: widget.toolbarOptions,
            showCursor: widget.showCursor,
            showSelectionHandles: _showSelectionHandles,
1345
            focusNode: _effectiveFocusNode,
1346 1347 1348 1349 1350 1351
            keyboardType: widget.keyboardType,
            textInputAction: widget.textInputAction,
            textCapitalization: widget.textCapitalization,
            style: textStyle,
            strutStyle: widget.strutStyle,
            textAlign: widget.textAlign,
1352
            textDirection: widget.textDirection,
1353 1354 1355 1356 1357 1358 1359 1360 1361 1362
            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,
1363
            magnifierConfiguration: widget.magnifierConfiguration ?? CupertinoTextField._iosMagnifierConfiguration,
1364 1365
            // Only show the selection highlight when the text field is focused.
            selectionColor: _effectiveFocusNode.hasFocus ? selectionColor : null,
1366
            selectionControls: widget.selectionEnabled
1367
              ? textSelectionControls : null,
1368 1369 1370 1371
            onChanged: widget.onChanged,
            onSelectionChanged: _handleSelectionChanged,
            onEditingComplete: widget.onEditingComplete,
            onSubmitted: widget.onSubmitted,
1372
            onTapOutside: widget.onTapOutside,
1373 1374 1375 1376 1377 1378
            inputFormatters: formatters,
            rendererIgnoresPointer: true,
            cursorWidth: widget.cursorWidth,
            cursorHeight: widget.cursorHeight,
            cursorRadius: widget.cursorRadius,
            cursorColor: cursorColor,
1379
            cursorOpacityAnimates: widget.cursorOpacityAnimates,
1380 1381 1382
            cursorOffset: cursorOffset,
            paintCursorAboveText: true,
            autocorrectionTextRectColor: selectionColor,
1383
            backgroundCursorColor: CupertinoDynamicColor.resolve(CupertinoColors.inactiveGray, context),
1384 1385 1386 1387 1388 1389 1390 1391
            selectionHeightStyle: widget.selectionHeightStyle,
            selectionWidthStyle: widget.selectionWidthStyle,
            scrollPadding: widget.scrollPadding,
            keyboardAppearance: keyboardAppearance,
            dragStartBehavior: widget.dragStartBehavior,
            scrollController: widget.scrollController,
            scrollPhysics: widget.scrollPhysics,
            enableInteractiveSelection: widget.enableInteractiveSelection,
1392
            autofillClient: this,
1393
            clipBehavior: widget.clipBehavior,
1394
            restorationId: 'editable',
1395
            scribbleEnabled: widget.scribbleEnabled,
1396
            enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,
1397
            contentInsertionConfiguration: widget.contentInsertionConfiguration,
1398
            contextMenuBuilder: widget.contextMenuBuilder,
1399
            spellCheckConfiguration: spellCheckConfiguration,
1400
          ),
1401 1402 1403 1404
        ),
      ),
    );

1405
    return Semantics(
1406 1407 1408 1409 1410 1411 1412
      enabled: enabled,
      onTap: !enabled || widget.readOnly ? null : () {
        if (!controller.selection.isValid) {
          controller.selection = TextSelection.collapsed(offset: controller.text.length);
        }
        _requestKeyboard();
      },
1413
      onDidGainAccessibilityFocus: handleDidGainAccessibilityFocus,
1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427
      child: TextFieldTapRegion(
        child: IgnorePointer(
          ignoring: !enabled,
          child: Container(
            decoration: effectiveDecoration,
            color: !enabled && effectiveDecoration == null ? disabledColor : null,
            child: _selectionGestureDetectorBuilder.buildGestureDetector(
              behavior: HitTestBehavior.translucent,
              child: Align(
                alignment: Alignment(-1.0, _textAlignVertical.y),
                widthFactor: 1.0,
                heightFactor: 1.0,
                child: _addTextDependentAttachments(paddedEditable, textStyle, placeholderStyle),
              ),
1428 1429
            ),
          ),
1430 1431 1432 1433 1434
        ),
      ),
    );
  }
}