Michael Goderbauer 19dfde6989
Remove unnecessary null checks in flutter/{foundation,services,physics} (#118910)
* Remove unnecessary null checks in flutter/foundation

* Remove unnecessary null checks in flutter/services

* Remove unnecessary null checks in flutter/physics
2023-01-23 19:29:48 +00:00

2347 lines
87 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:io' show Platform;
import 'dart:ui' show
FontWeight,
Offset,
Rect,
Size,
TextAlign,
TextDirection;
import 'package:flutter/foundation.dart';
import 'package:vector_math/vector_math_64.dart' show Matrix4;
import 'autofill.dart';
import 'clipboard.dart' show Clipboard;
import 'message_codec.dart';
import 'platform_channel.dart';
import 'system_channels.dart';
import 'text_editing.dart';
import 'text_editing_delta.dart';
export 'dart:ui' show Brightness, FontWeight, Offset, Rect, Size, TextAlign, TextDirection, TextPosition, TextRange;
export 'package:vector_math/vector_math_64.dart' show Matrix4;
export 'autofill.dart' show AutofillConfiguration, AutofillScope;
export 'text_editing.dart' show TextSelection;
// TODO(a14n): the following export leads to Segmentation fault, see https://github.com/flutter/flutter/issues/106332
// export 'text_editing_delta.dart' show TextEditingDelta;
/// Indicates how to handle the intelligent replacement of dashes in text input.
///
/// See also:
///
/// * [TextField.smartDashesType]
/// * [CupertinoTextField.smartDashesType]
/// * [EditableText.smartDashesType]
/// * [SmartQuotesType]
/// * <https://developer.apple.com/documentation/uikit/uitextinputtraits>
enum SmartDashesType {
/// Smart dashes is disabled.
///
/// This corresponds to the
/// ["no" value of UITextSmartDashesType](https://developer.apple.com/documentation/uikit/uitextsmartdashestype/no).
disabled,
/// Smart dashes is enabled.
///
/// This corresponds to the
/// ["yes" value of UITextSmartDashesType](https://developer.apple.com/documentation/uikit/uitextsmartdashestype/yes).
enabled,
}
/// Indicates how to handle the intelligent replacement of quotes in text input.
///
/// See also:
///
/// * [TextField.smartQuotesType]
/// * [CupertinoTextField.smartQuotesType]
/// * [EditableText.smartQuotesType]
/// * <https://developer.apple.com/documentation/uikit/uitextinputtraits>
enum SmartQuotesType {
/// Smart quotes is disabled.
///
/// This corresponds to the
/// ["no" value of UITextSmartQuotesType](https://developer.apple.com/documentation/uikit/uitextsmartquotestype/no).
disabled,
/// Smart quotes is enabled.
///
/// This corresponds to the
/// ["yes" value of UITextSmartQuotesType](https://developer.apple.com/documentation/uikit/uitextsmartquotestype/yes).
enabled,
}
/// The type of information for which to optimize the text input control.
///
/// On Android, behavior may vary across device and keyboard provider.
///
/// This class stays as close to `Enum` interface as possible, and allows
/// for additional flags for some input types. For example, numeric input
/// can specify whether it supports decimal numbers and/or signed numbers.
@immutable
class TextInputType {
const TextInputType._(this.index)
: signed = null,
decimal = null;
/// Optimize for numerical information.
///
/// Requests a numeric keyboard with additional settings.
/// The [signed] and [decimal] parameters are optional.
const TextInputType.numberWithOptions({
this.signed = false,
this.decimal = false,
}) : index = 2;
/// Enum value index, corresponds to one of the [values].
final int index;
/// The number is signed, allowing a positive or negative sign at the start.
///
/// This flag is only used for the [number] input type, otherwise `null`.
/// Use `const TextInputType.numberWithOptions(signed: true)` to set this.
final bool? signed;
/// The number is decimal, allowing a decimal point to provide fractional.
///
/// This flag is only used for the [number] input type, otherwise `null`.
/// Use `const TextInputType.numberWithOptions(decimal: true)` to set this.
final bool? decimal;
/// Optimize for textual information.
///
/// Requests the default platform keyboard.
static const TextInputType text = TextInputType._(0);
/// Optimize for multiline textual information.
///
/// Requests the default platform keyboard, but accepts newlines when the
/// enter key is pressed. This is the input type used for all multiline text
/// fields.
static const TextInputType multiline = TextInputType._(1);
/// Optimize for unsigned numerical information without a decimal point.
///
/// Requests a default keyboard with ready access to the number keys.
/// Additional options, such as decimal point and/or positive/negative
/// signs, can be requested using [TextInputType.numberWithOptions].
static const TextInputType number = TextInputType.numberWithOptions();
/// Optimize for telephone numbers.
///
/// Requests a keyboard with ready access to the number keys, "*", and "#".
static const TextInputType phone = TextInputType._(3);
/// Optimize for date and time information.
///
/// On iOS, requests the default keyboard.
///
/// On Android, requests a keyboard with ready access to the number keys,
/// ":", and "-".
static const TextInputType datetime = TextInputType._(4);
/// Optimize for email addresses.
///
/// Requests a keyboard with ready access to the "@" and "." keys.
static const TextInputType emailAddress = TextInputType._(5);
/// Optimize for URLs.
///
/// Requests a keyboard with ready access to the "/" and "." keys.
static const TextInputType url = TextInputType._(6);
/// Optimize for passwords that are visible to the user.
///
/// Requests a keyboard with ready access to both letters and numbers.
static const TextInputType visiblePassword = TextInputType._(7);
/// Optimized for a person's name.
///
/// On iOS, requests the
/// [UIKeyboardType.namePhonePad](https://developer.apple.com/documentation/uikit/uikeyboardtype/namephonepad)
/// keyboard, a keyboard optimized for entering a persons name or phone number.
/// Does not support auto-capitalization.
///
/// On Android, requests a keyboard optimized for
/// [TYPE_TEXT_VARIATION_PERSON_NAME](https://developer.android.com/reference/android/text/InputType#TYPE_TEXT_VARIATION_PERSON_NAME).
static const TextInputType name = TextInputType._(8);
/// Optimized for postal mailing addresses.
///
/// On iOS, requests the default keyboard.
///
/// On Android, requests a keyboard optimized for
/// [TYPE_TEXT_VARIATION_POSTAL_ADDRESS](https://developer.android.com/reference/android/text/InputType#TYPE_TEXT_VARIATION_POSTAL_ADDRESS).
static const TextInputType streetAddress = TextInputType._(9);
/// Prevent the OS from showing the on-screen virtual keyboard.
static const TextInputType none = TextInputType._(10);
/// All possible enum values.
static const List<TextInputType> values = <TextInputType>[
text, multiline, number, phone, datetime, emailAddress, url, visiblePassword, name, streetAddress, none,
];
// Corresponding string name for each of the [values].
static const List<String> _names = <String>[
'text', 'multiline', 'number', 'phone', 'datetime', 'emailAddress', 'url', 'visiblePassword', 'name', 'address', 'none',
];
// Enum value name, this is what enum.toString() would normally return.
String get _name => 'TextInputType.${_names[index]}';
/// Returns a representation of this object as a JSON object.
Map<String, dynamic> toJson() {
return <String, dynamic>{
'name': _name,
'signed': signed,
'decimal': decimal,
};
}
@override
String toString() {
return '${objectRuntimeType(this, 'TextInputType')}('
'name: $_name, '
'signed: $signed, '
'decimal: $decimal)';
}
@override
bool operator ==(Object other) {
return other is TextInputType
&& other.index == index
&& other.signed == signed
&& other.decimal == decimal;
}
@override
int get hashCode => Object.hash(index, signed, decimal);
}
/// An action the user has requested the text input control to perform.
///
/// Each action represents a logical meaning, and also configures the soft
/// keyboard to display a certain kind of action button. The visual appearance
/// of the action button might differ between versions of the same OS.
///
/// Despite the logical meaning of each action, choosing a particular
/// [TextInputAction] does not necessarily cause any specific behavior to
/// happen, other than changing the focus when appropriate. It is up to the
/// developer to ensure that the behavior that occurs when an action button is
/// pressed is appropriate for the action button chosen.
///
/// For example: If the user presses the keyboard action button on iOS when it
/// reads "Emergency Call", the result should not be a focus change to the next
/// TextField. This behavior is not logically appropriate for a button that says
/// "Emergency Call".
///
/// See [EditableText] for more information about customizing action button
/// behavior.
///
/// Most [TextInputAction]s are supported equally by both Android and iOS.
/// However, there is not a complete, direct mapping between Android's IME input
/// types and iOS's keyboard return types. Therefore, some [TextInputAction]s
/// are inappropriate for one of the platforms. If a developer chooses an
/// inappropriate [TextInputAction] when running in debug mode, an error will be
/// thrown. If the same thing is done in release mode, then instead of sending
/// the inappropriate value, Android will use "unspecified" on the platform
/// side and iOS will use "default" on the platform side.
///
/// See also:
///
/// * [TextInput], which configures the platform's keyboard setup.
/// * [EditableText], which invokes callbacks when the action button is pressed.
//
// This class has been cloned to `flutter_driver/lib/src/common/action.dart` as `TextInputAction`,
// and must be kept in sync.
enum TextInputAction {
/// Logical meaning: There is no relevant input action for the current input
/// source, e.g., [TextField].
///
/// Android: Corresponds to Android's "IME_ACTION_NONE". The keyboard setup
/// is decided by the OS. The keyboard will likely show a return key.
///
/// iOS: iOS does not have a keyboard return type of "none." It is
/// inappropriate to choose this [TextInputAction] when running on iOS.
none,
/// Logical meaning: Let the OS decide which action is most appropriate.
///
/// Android: Corresponds to Android's "IME_ACTION_UNSPECIFIED". The OS chooses
/// which keyboard action to display. The decision will likely be a done
/// button or a return key.
///
/// iOS: Corresponds to iOS's "UIReturnKeyDefault". The title displayed in
/// the action button is "return".
unspecified,
/// Logical meaning: The user is done providing input to a group of inputs
/// (like a form). Some kind of finalization behavior should now take place.
///
/// Android: Corresponds to Android's "IME_ACTION_DONE". The OS displays a
/// button that represents completion, e.g., a checkmark button.
///
/// iOS: Corresponds to iOS's "UIReturnKeyDone". The title displayed in the
/// action button is "Done".
done,
/// Logical meaning: The user has entered some text that represents a
/// destination, e.g., a restaurant name. The "go" button is intended to take
/// the user to a part of the app that corresponds to this destination.
///
/// Android: Corresponds to Android's "IME_ACTION_GO". The OS displays a
/// button that represents taking "the user to the target of the text they
/// typed", e.g., a right-facing arrow button.
///
/// iOS: Corresponds to iOS's "UIReturnKeyGo". The title displayed in the
/// action button is "Go".
go,
/// Logical meaning: Execute a search query.
///
/// Android: Corresponds to Android's "IME_ACTION_SEARCH". The OS displays a
/// button that represents a search, e.g., a magnifying glass button.
///
/// iOS: Corresponds to iOS's "UIReturnKeySearch". The title displayed in the
/// action button is "Search".
search,
/// Logical meaning: Sends something that the user has composed, e.g., an
/// email or a text message.
///
/// Android: Corresponds to Android's "IME_ACTION_SEND". The OS displays a
/// button that represents sending something, e.g., a paper plane button.
///
/// iOS: Corresponds to iOS's "UIReturnKeySend". The title displayed in the
/// action button is "Send".
send,
/// Logical meaning: The user is done with the current input source and wants
/// to move to the next one.
///
/// Moves the focus to the next focusable item in the same [FocusScope].
///
/// Android: Corresponds to Android's "IME_ACTION_NEXT". The OS displays a
/// button that represents moving forward, e.g., a right-facing arrow button.
///
/// iOS: Corresponds to iOS's "UIReturnKeyNext". The title displayed in the
/// action button is "Next".
next,
/// Logical meaning: The user wishes to return to the previous input source
/// in the group, e.g., a form with multiple [TextField]s.
///
/// Moves the focus to the previous focusable item in the same [FocusScope].
///
/// Android: Corresponds to Android's "IME_ACTION_PREVIOUS". The OS displays a
/// button that represents moving backward, e.g., a left-facing arrow button.
///
/// iOS: iOS does not have a keyboard return type of "previous." It is
/// inappropriate to choose this [TextInputAction] when running on iOS.
previous,
/// Logical meaning: In iOS apps, it is common for a "Back" button and
/// "Continue" button to appear at the top of the screen. However, when the
/// keyboard is open, these buttons are often hidden off-screen. Therefore,
/// the purpose of the "Continue" return key on iOS is to make the "Continue"
/// button available when the user is entering text.
///
/// Historical context aside, [TextInputAction.continueAction] can be used any
/// time that the term "Continue" seems most appropriate for the given action.
///
/// Android: Android does not have an IME input type of "continue." It is
/// inappropriate to choose this [TextInputAction] when running on Android.
///
/// iOS: Corresponds to iOS's "UIReturnKeyContinue". The title displayed in the
/// action button is "Continue". This action is only available on iOS 9.0+.
///
/// The reason that this value has "Action" post-fixed to it is because
/// "continue" is a reserved word in Dart, as well as many other languages.
continueAction,
/// Logical meaning: The user wants to join something, e.g., a wireless
/// network.
///
/// Android: Android does not have an IME input type of "join." It is
/// inappropriate to choose this [TextInputAction] when running on Android.
///
/// iOS: Corresponds to iOS's "UIReturnKeyJoin". The title displayed in the
/// action button is "Join".
join,
/// Logical meaning: The user wants routing options, e.g., driving directions.
///
/// Android: Android does not have an IME input type of "route." It is
/// inappropriate to choose this [TextInputAction] when running on Android.
///
/// iOS: Corresponds to iOS's "UIReturnKeyRoute". The title displayed in the
/// action button is "Route".
route,
/// Logical meaning: Initiate a call to emergency services.
///
/// Android: Android does not have an IME input type of "emergencyCall." It is
/// inappropriate to choose this [TextInputAction] when running on Android.
///
/// iOS: Corresponds to iOS's "UIReturnKeyEmergencyCall". The title displayed
/// in the action button is "Emergency Call".
emergencyCall,
/// Logical meaning: Insert a newline character in the focused text input,
/// e.g., [TextField].
///
/// Android: Corresponds to Android's "IME_ACTION_NONE". The OS displays a
/// button that represents a new line, e.g., a carriage return button.
///
/// iOS: Corresponds to iOS's "UIReturnKeyDefault". The title displayed in the
/// action button is "return".
///
/// The term [TextInputAction.newline] exists in Flutter but not in Android
/// or iOS. The reason for introducing this term is so that developers can
/// achieve the common result of inserting new lines without needing to
/// understand the various IME actions on Android and return keys on iOS.
/// Thus, [TextInputAction.newline] is a convenience term that alleviates the
/// need to understand the underlying platforms to achieve this common behavior.
newline,
}
/// Configures how the platform keyboard will select an uppercase or
/// lowercase keyboard.
///
/// Only supports text keyboards, other keyboard types will ignore this
/// configuration. Capitalization is locale-aware.
enum TextCapitalization {
/// Defaults to an uppercase keyboard for the first letter of each word.
///
/// Corresponds to `InputType.TYPE_TEXT_FLAG_CAP_WORDS` on Android, and
/// `UITextAutocapitalizationTypeWords` on iOS.
words,
/// Defaults to an uppercase keyboard for the first letter of each sentence.
///
/// Corresponds to `InputType.TYPE_TEXT_FLAG_CAP_SENTENCES` on Android, and
/// `UITextAutocapitalizationTypeSentences` on iOS.
sentences,
/// Defaults to an uppercase keyboard for each character.
///
/// Corresponds to `InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS` on Android, and
/// `UITextAutocapitalizationTypeAllCharacters` on iOS.
characters,
/// Defaults to a lowercase keyboard.
none,
}
/// Controls the visual appearance of the text input control.
///
/// Many [TextInputAction]s are common between Android and iOS. However, if an
/// [inputAction] is provided that is not supported by the current
/// platform in debug mode, an error will be thrown when the corresponding
/// text input is attached. For example, providing iOS's "emergencyCall"
/// action when running on an Android device will result in an error when in
/// debug mode. In release mode, incompatible [TextInputAction]s are replaced
/// either with "unspecified" on Android, or "default" on iOS. Appropriate
/// [inputAction]s can be chosen by checking the current platform and then
/// selecting the appropriate action.
///
/// See also:
///
/// * [TextInput.attach]
/// * [TextInputAction]
@immutable
class TextInputConfiguration {
/// Creates configuration information for a text input control.
///
/// All arguments have default values, except [actionLabel]. Only
/// [actionLabel] may be null.
const TextInputConfiguration({
this.inputType = TextInputType.text,
this.readOnly = false,
this.obscureText = false,
this.autocorrect = true,
SmartDashesType? smartDashesType,
SmartQuotesType? smartQuotesType,
this.enableSuggestions = true,
this.enableInteractiveSelection = true,
this.actionLabel,
this.inputAction = TextInputAction.done,
this.keyboardAppearance = Brightness.light,
this.textCapitalization = TextCapitalization.none,
this.autofillConfiguration = AutofillConfiguration.disabled,
this.enableIMEPersonalizedLearning = true,
this.enableDeltaModel = false,
}) : smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled);
/// The type of information for which to optimize the text input control.
final TextInputType inputType;
/// Whether the text field can be edited or not.
///
/// Defaults to false.
final bool readOnly;
/// Whether to hide the text being edited (e.g., for passwords).
///
/// Defaults to false.
final bool obscureText;
/// Whether to enable autocorrection.
///
/// Defaults to true.
final bool autocorrect;
/// The configuration to use for autofill.
///
/// Defaults to null, in which case no autofill information will be provided
/// to the platform. This will prevent the corresponding input field from
/// participating in autofills triggered by other fields. Additionally, on
/// Android and web, setting [autofillConfiguration] to null disables autofill.
final AutofillConfiguration autofillConfiguration;
/// {@template flutter.services.TextInputConfiguration.smartDashesType}
/// Whether to allow the platform to automatically format dashes.
///
/// This flag only affects iOS versions 11 and above. It sets
/// [`UITextSmartDashesType`](https://developer.apple.com/documentation/uikit/uitextsmartdashestype?language=objc)
/// in the engine. When true, it passes
/// [`UITextSmartDashesTypeYes`](https://developer.apple.com/documentation/uikit/uitextsmartdashestype/uitextsmartdashestypeyes?language=objc),
/// and when false, it passes
/// [`UITextSmartDashesTypeNo`](https://developer.apple.com/documentation/uikit/uitextsmartdashestype/uitextsmartdashestypeno?language=objc).
///
/// As an example of what this does, two consecutive hyphen characters will be
/// automatically replaced with one en dash, and three consecutive hyphens
/// will become one em dash.
///
/// Defaults to true, unless [obscureText] is true, when it defaults to false.
/// This is to avoid the problem where password fields receive autoformatted
/// characters.
///
/// See also:
///
/// * [smartQuotesType]
/// * <https://developer.apple.com/documentation/uikit/uitextinputtraits>
/// {@endtemplate}
final SmartDashesType smartDashesType;
/// {@template flutter.services.TextInputConfiguration.smartQuotesType}
/// Whether to allow the platform to automatically format quotes.
///
/// This flag only affects iOS. It sets
/// [`UITextSmartQuotesType`](https://developer.apple.com/documentation/uikit/uitextsmartquotestype?language=objc)
/// in the engine. When true, it passes
/// [`UITextSmartQuotesTypeYes`](https://developer.apple.com/documentation/uikit/uitextsmartquotestype/uitextsmartquotestypeyes?language=objc),
/// and when false, it passes
/// [`UITextSmartQuotesTypeNo`](https://developer.apple.com/documentation/uikit/uitextsmartquotestype/uitextsmartquotestypeno?language=objc).
///
/// As an example of what this does, a standard vertical double quote
/// character will be automatically replaced by a left or right double quote
/// depending on its position in a word.
///
/// Defaults to true, unless [obscureText] is true, when it defaults to false.
/// This is to avoid the problem where password fields receive autoformatted
/// characters.
///
/// See also:
///
/// * [smartDashesType]
/// * <https://developer.apple.com/documentation/uikit/uitextinputtraits>
/// {@endtemplate}
final SmartQuotesType smartQuotesType;
/// {@template flutter.services.TextInputConfiguration.enableSuggestions}
/// Whether to show input suggestions as the user types.
///
/// This flag only affects Android. On iOS, suggestions are tied directly to
/// [autocorrect], so that suggestions are only shown when [autocorrect] is
/// true. On Android autocorrection and suggestion are controlled separately.
///
/// Defaults to true. Cannot be null.
///
/// See also:
///
/// * <https://developer.android.com/reference/android/text/InputType.html#TYPE_TEXT_FLAG_NO_SUGGESTIONS>
/// {@endtemplate}
final bool enableSuggestions;
/// Whether a user can change its selection.
///
/// This flag only affects iOS VoiceOver. On Android Talkback, the selection
/// change is sent through semantics actions and is directly disabled from
/// the widget side.
///
/// Defaults to true. Cannot be null.
final bool enableInteractiveSelection;
/// What text to display in the text input control's action button.
final String? actionLabel;
/// What kind of action to request for the action button on the IME.
final TextInputAction inputAction;
/// Specifies how platforms may automatically capitalize text entered by the
/// user.
///
/// Defaults to [TextCapitalization.none].
///
/// See also:
///
/// * [TextCapitalization], for a description of each capitalization behavior.
final TextCapitalization textCapitalization;
/// The appearance of the keyboard.
///
/// This setting is only honored on iOS devices.
///
/// Defaults to [Brightness.light].
final Brightness keyboardAppearance;
/// {@template flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
/// Whether to enable that the IME update personalized data such as typing
/// history and user dictionary data.
///
/// This flag only affects Android. On iOS, there is no equivalent flag.
///
/// Defaults to true. Cannot be null.
///
/// See also:
///
/// * <https://developer.android.com/reference/android/view/inputmethod/EditorInfo#IME_FLAG_NO_PERSONALIZED_LEARNING>
/// {@endtemplate}
final bool enableIMEPersonalizedLearning;
/// Creates a copy of this [TextInputConfiguration] with the given fields
/// replaced with new values.
TextInputConfiguration copyWith({
TextInputType? inputType,
bool? readOnly,
bool? obscureText,
bool? autocorrect,
SmartDashesType? smartDashesType,
SmartQuotesType? smartQuotesType,
bool? enableSuggestions,
bool? enableInteractiveSelection,
String? actionLabel,
TextInputAction? inputAction,
Brightness? keyboardAppearance,
TextCapitalization? textCapitalization,
bool? enableIMEPersonalizedLearning,
AutofillConfiguration? autofillConfiguration,
bool? enableDeltaModel,
}) {
return TextInputConfiguration(
inputType: inputType ?? this.inputType,
readOnly: readOnly ?? this.readOnly,
obscureText: obscureText ?? this.obscureText,
autocorrect: autocorrect ?? this.autocorrect,
smartDashesType: smartDashesType ?? this.smartDashesType,
smartQuotesType: smartQuotesType ?? this.smartQuotesType,
enableSuggestions: enableSuggestions ?? this.enableSuggestions,
enableInteractiveSelection: enableInteractiveSelection ?? this.enableInteractiveSelection,
inputAction: inputAction ?? this.inputAction,
textCapitalization: textCapitalization ?? this.textCapitalization,
keyboardAppearance: keyboardAppearance ?? this.keyboardAppearance,
enableIMEPersonalizedLearning: enableIMEPersonalizedLearning?? this.enableIMEPersonalizedLearning,
autofillConfiguration: autofillConfiguration ?? this.autofillConfiguration,
enableDeltaModel: enableDeltaModel ?? this.enableDeltaModel,
);
}
/// Whether to enable that the engine sends text input updates to the
/// framework as [TextEditingDelta]'s or as one [TextEditingValue].
///
/// Enabling this flag results in granular text updates being received from the
/// platform's text input control.
///
/// When this is enabled:
/// * You must implement [DeltaTextInputClient] and not [TextInputClient] to
/// receive granular updates from the platform's text input.
/// * Platform text input updates will come through
/// [DeltaTextInputClient.updateEditingValueWithDeltas].
/// * If [TextInputClient] is implemented with this property enabled then
/// you will experience unexpected behavior as [TextInputClient] does not implement
/// a delta channel.
///
/// When this is disabled:
/// * If [DeltaTextInputClient] is implemented then updates for the
/// editing state will continue to come through the
/// [DeltaTextInputClient.updateEditingValue] channel.
/// * If [TextInputClient] is implemented then updates for the editing
/// state will come through [TextInputClient.updateEditingValue].
///
/// Defaults to false. Cannot be null.
final bool enableDeltaModel;
/// Returns a representation of this object as a JSON object.
Map<String, dynamic> toJson() {
final Map<String, dynamic>? autofill = autofillConfiguration.toJson();
return <String, dynamic>{
'inputType': inputType.toJson(),
'readOnly': readOnly,
'obscureText': obscureText,
'autocorrect': autocorrect,
'smartDashesType': smartDashesType.index.toString(),
'smartQuotesType': smartQuotesType.index.toString(),
'enableSuggestions': enableSuggestions,
'enableInteractiveSelection': enableInteractiveSelection,
'actionLabel': actionLabel,
'inputAction': inputAction.toString(),
'textCapitalization': textCapitalization.toString(),
'keyboardAppearance': keyboardAppearance.toString(),
'enableIMEPersonalizedLearning': enableIMEPersonalizedLearning,
if (autofill != null) 'autofill': autofill,
'enableDeltaModel' : enableDeltaModel,
};
}
}
TextAffinity? _toTextAffinity(String? affinity) {
switch (affinity) {
case 'TextAffinity.downstream':
return TextAffinity.downstream;
case 'TextAffinity.upstream':
return TextAffinity.upstream;
}
return null;
}
/// A floating cursor state the user has induced by force pressing an iOS
/// keyboard.
enum FloatingCursorDragState {
/// A user has just activated a floating cursor.
Start,
/// A user is dragging a floating cursor.
Update,
/// A user has lifted their finger off the screen after using a floating
/// cursor.
End,
}
/// The current state and position of the floating cursor.
class RawFloatingCursorPoint {
/// Creates information for setting the position and state of a floating
/// cursor.
///
/// [state] must not be null and [offset] must not be null if the state is
/// [FloatingCursorDragState.Update].
RawFloatingCursorPoint({
this.offset,
required this.state,
}) : assert(state != FloatingCursorDragState.Update || offset != null);
/// The raw position of the floating cursor as determined by the iOS sdk.
final Offset? offset;
/// The state of the floating cursor.
final FloatingCursorDragState state;
}
/// The current text, selection, and composing state for editing a run of text.
@immutable
class TextEditingValue {
/// Creates information for editing a run of text.
///
/// The selection and composing range must be within the text. This is not
/// checked during construction, and must be guaranteed by the caller.
///
/// The [text], [selection], and [composing] arguments must not be null but
/// each have default values.
///
/// The default value of [selection] is `TextSelection.collapsed(offset: -1)`.
/// This indicates that there is no selection at all.
const TextEditingValue({
this.text = '',
this.selection = const TextSelection.collapsed(offset: -1),
this.composing = TextRange.empty,
});
/// Creates an instance of this class from a JSON object.
factory TextEditingValue.fromJSON(Map<String, dynamic> encoded) {
final String text = encoded['text'] as String;
final TextSelection selection = TextSelection(
baseOffset: encoded['selectionBase'] as int? ?? -1,
extentOffset: encoded['selectionExtent'] as int? ?? -1,
affinity: _toTextAffinity(encoded['selectionAffinity'] as String?) ?? TextAffinity.downstream,
isDirectional: encoded['selectionIsDirectional'] as bool? ?? false,
);
final TextRange composing = TextRange(
start: encoded['composingBase'] as int? ?? -1,
end: encoded['composingExtent'] as int? ?? -1,
);
assert(_textRangeIsValid(selection, text));
assert(_textRangeIsValid(composing, text));
return TextEditingValue(
text: text,
selection: selection,
composing: composing,
);
}
/// The current text being edited.
final String text;
/// The range of text that is currently selected.
///
/// When [selection] is a [TextSelection] that has the same non-negative
/// `baseOffset` and `extentOffset`, the [selection] property represents the
/// caret position.
///
/// If the current [selection] has a negative `baseOffset` or `extentOffset`,
/// then the text currently does not have a selection or a caret location, and
/// most text editing operations that rely on the current selection (for
/// instance, insert a character at the caret location) will do nothing.
final TextSelection selection;
/// The range of text that is still being composed.
///
/// Composing regions are created by input methods (IMEs) to indicate the text
/// within a certain range is provisional. For instance, the Android Gboard
/// app's English keyboard puts the current word under the caret into a
/// composing region to indicate the word is subject to autocorrect or
/// prediction changes.
///
/// Composing regions can also be used for performing multistage input, which
/// is typically used by IMEs designed for phonetic keyboard to enter
/// ideographic symbols. As an example, many CJK keyboards require the user to
/// enter a Latin alphabet sequence and then convert it to CJK characters. On
/// iOS, the default software keyboards do not have a dedicated view to show
/// the unfinished Latin sequence, so it's displayed directly in the text
/// field, inside of a composing region.
///
/// The composing region should typically only be changed by the IME, or the
/// user via interacting with the IME.
///
/// If the range represented by this property is [TextRange.empty], then the
/// text is not currently being composed.
final TextRange composing;
/// A value that corresponds to the empty string with no selection and no composing range.
static const TextEditingValue empty = TextEditingValue();
/// Creates a copy of this value but with the given fields replaced with the new values.
TextEditingValue copyWith({
String? text,
TextSelection? selection,
TextRange? composing,
}) {
return TextEditingValue(
text: text ?? this.text,
selection: selection ?? this.selection,
composing: composing ?? this.composing,
);
}
/// Whether the [composing] range is a valid range within [text].
///
/// Returns true if and only if the [composing] range is normalized, its start
/// is greater than or equal to 0, and its end is less than or equal to
/// [text]'s length.
///
/// If this property is false while the [composing] range's `isValid` is true,
/// it usually indicates the current [composing] range is invalid because of a
/// programming error.
bool get isComposingRangeValid => composing.isValid && composing.isNormalized && composing.end <= text.length;
/// Returns a new [TextEditingValue], which is this [TextEditingValue] with
/// its [text] partially replaced by the `replacementString`.
///
/// The `replacementRange` parameter specifies the range of the
/// [TextEditingValue.text] that needs to be replaced.
///
/// The `replacementString` parameter specifies the string to replace the
/// given range of text with.
///
/// This method also adjusts the selection range and the composing range of the
/// resulting [TextEditingValue], such that they point to the same substrings
/// as the corresponding ranges in the original [TextEditingValue]. For
/// example, if the original [TextEditingValue] is "Hello world" with the word
/// "world" selected, replacing "Hello" with a different string using this
/// method will not change the selected word.
///
/// This method does nothing if the given `replacementRange` is not
/// [TextRange.isValid].
TextEditingValue replaced(TextRange replacementRange, String replacementString) {
if (!replacementRange.isValid) {
return this;
}
final String newText = text.replaceRange(replacementRange.start, replacementRange.end, replacementString);
if (replacementRange.end - replacementRange.start == replacementString.length) {
return copyWith(text: newText);
}
int adjustIndex(int originalIndex) {
// The length added by adding the replacementString.
final int replacedLength = originalIndex <= replacementRange.start && originalIndex < replacementRange.end ? 0 : replacementString.length;
// The length removed by removing the replacementRange.
final int removedLength = originalIndex.clamp(replacementRange.start, replacementRange.end) - replacementRange.start; // ignore_clamp_double_lint
return originalIndex + replacedLength - removedLength;
}
final TextSelection adjustedSelection = TextSelection(
baseOffset: adjustIndex(selection.baseOffset),
extentOffset: adjustIndex(selection.extentOffset),
);
final TextRange adjustedComposing = TextRange(
start: adjustIndex(composing.start),
end: adjustIndex(composing.end),
);
assert(_textRangeIsValid(adjustedSelection, newText));
assert(_textRangeIsValid(adjustedComposing, newText));
return TextEditingValue(
text: newText,
selection: adjustedSelection,
composing: adjustedComposing,
);
}
/// Returns a representation of this object as a JSON object.
Map<String, dynamic> toJSON() {
assert(_textRangeIsValid(selection, text));
assert(_textRangeIsValid(composing, text));
return <String, dynamic>{
'text': text,
'selectionBase': selection.baseOffset,
'selectionExtent': selection.extentOffset,
'selectionAffinity': selection.affinity.toString(),
'selectionIsDirectional': selection.isDirectional,
'composingBase': composing.start,
'composingExtent': composing.end,
};
}
@override
String toString() => '${objectRuntimeType(this, 'TextEditingValue')}(text: \u2524$text\u251C, selection: $selection, composing: $composing)';
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
return other is TextEditingValue
&& other.text == text
&& other.selection == selection
&& other.composing == composing;
}
@override
int get hashCode => Object.hash(
text.hashCode,
selection.hashCode,
composing.hashCode,
);
// Verify that the given range is within the text.
//
// The verification can't be perform during the constructor of
// [TextEditingValue], which are `const` and are allowed to retrieve
// properties of [TextRange]s. [TextEditingValue] should perform this
// wherever it is building other values (such as toJson) or is built in a
// non-const way (such as fromJson).
static bool _textRangeIsValid(TextRange range, String text) {
if (range.start == -1 && range.end == -1) {
return true;
}
assert(range.start >= 0 && range.start <= text.length,
'Range start ${range.start} is out of text of length ${text.length}');
assert(range.end >= 0 && range.end <= text.length,
'Range end ${range.end} is out of text of length ${text.length}');
return true;
}
}
/// Indicates what triggered the change in selected text (including changes to
/// the cursor location).
enum SelectionChangedCause {
/// The user tapped on the text and that caused the selection (or the location
/// of the cursor) to change.
tap,
/// The user tapped twice in quick succession on the text and that caused
/// the selection (or the location of the cursor) to change.
doubleTap,
/// The user long-pressed the text and that caused the selection (or the
/// location of the cursor) to change.
longPress,
/// The user force-pressed the text and that caused the selection (or the
/// location of the cursor) to change.
forcePress,
/// The user used the keyboard to change the selection or the location of the
/// cursor.
///
/// Keyboard-triggered selection changes may be caused by the IME as well as
/// by accessibility tools (e.g. TalkBack on Android).
keyboard,
/// The user used the selection toolbar to change the selection or the
/// location of the cursor.
///
/// An example is when the user taps on select all in the tool bar.
toolbar,
/// The user used the mouse to change the selection by dragging over a piece
/// of text.
drag,
/// The user used iPadOS 14+ Scribble to change the selection.
scribble,
}
/// A mixin for manipulating the selection, provided for toolbar or shortcut
/// keys.
mixin TextSelectionDelegate {
/// Gets the current text input.
TextEditingValue get textEditingValue;
/// Indicates that the user has requested the delegate to replace its current
/// text editing state with [value].
///
/// The new [value] is treated as user input and thus may subject to input
/// formatting.
///
/// See also:
///
/// * [EditableTextState.userUpdateTextEditingValue]: an implementation that
/// applies additional pre-processing to the specified [value], before
/// updating the text editing state.
void userUpdateTextEditingValue(TextEditingValue value, SelectionChangedCause cause);
/// Hides the text selection toolbar.
///
/// By default, hideHandles is true, and the toolbar is hidden along with its
/// handles. If hideHandles is set to false, then the toolbar will be hidden
/// but the handles will remain.
void hideToolbar([bool hideHandles = true]);
/// Brings the provided [TextPosition] into the visible area of the text
/// input.
void bringIntoView(TextPosition position);
/// Whether cut is enabled, must not be null.
bool get cutEnabled => true;
/// Whether copy is enabled, must not be null.
bool get copyEnabled => true;
/// Whether paste is enabled, must not be null.
bool get pasteEnabled => true;
/// Whether select all is enabled, must not be null.
bool get selectAllEnabled => true;
/// Cut current selection to [Clipboard].
///
/// If and only if [cause] is [SelectionChangedCause.toolbar], the toolbar
/// will be hidden and the current selection will be scrolled into view.
void cutSelection(SelectionChangedCause cause);
/// Paste text from [Clipboard].
///
/// If there is currently a selection, it will be replaced.
///
/// If and only if [cause] is [SelectionChangedCause.toolbar], the toolbar
/// will be hidden and the current selection will be scrolled into view.
Future<void> pasteText(SelectionChangedCause cause);
/// Set the current selection to contain the entire text value.
///
/// If and only if [cause] is [SelectionChangedCause.toolbar], the selection
/// will be scrolled into view.
void selectAll(SelectionChangedCause cause);
/// Copy current selection to [Clipboard].
///
/// If [cause] is [SelectionChangedCause.toolbar], the position of
/// [bringIntoView] to selection will be called and hide toolbar.
void copySelection(SelectionChangedCause cause);
}
/// An interface to receive information from [TextInput].
///
/// If [TextInputConfiguration.enableDeltaModel] is set to true,
/// [DeltaTextInputClient] must be implemented instead of this class.
///
/// See also:
///
/// * [TextInput.attach]
/// * [EditableText], a [TextInputClient] implementation.
/// * [DeltaTextInputClient], a [TextInputClient] extension that receives
/// granular information from the platform's text input.
mixin TextInputClient {
/// The current state of the [TextEditingValue] held by this client.
TextEditingValue? get currentTextEditingValue;
/// The [AutofillScope] this [TextInputClient] belongs to, if any.
///
/// It should return null if this [TextInputClient] does not need autofill
/// support. For a [TextInputClient] that supports autofill, returning null
/// causes it to participate in autofill alone.
///
/// See also:
///
/// * [AutofillGroup], a widget that creates an [AutofillScope] for its
/// descendent autofillable [TextInputClient]s.
AutofillScope? get currentAutofillScope;
/// Requests that this client update its editing state to the given value.
///
/// The new [value] is treated as user input and thus may subject to input
/// formatting.
void updateEditingValue(TextEditingValue value);
/// Requests that this client perform the given action.
void performAction(TextInputAction action);
/// Request from the input method that this client perform the given private
/// command.
///
/// This can be used to provide domain-specific features that are only known
/// between certain input methods and their clients.
///
/// See also:
/// * [performPrivateCommand](https://developer.android.com/reference/android/view/inputmethod/InputConnection#performPrivateCommand\(java.lang.String,%20android.os.Bundle\)),
/// which is the Android documentation for performPrivateCommand, used to
/// send a command from the input method.
/// * [sendAppPrivateCommand](https://developer.android.com/reference/android/view/inputmethod/InputMethodManager#sendAppPrivateCommand),
/// which is the Android documentation for sendAppPrivateCommand, used to
/// send a command to the input method.
void performPrivateCommand(String action, Map<String, dynamic> data);
/// Updates the floating cursor position and state.
void updateFloatingCursor(RawFloatingCursorPoint point);
/// Requests that this client display a prompt rectangle for the given text range,
/// to indicate the range of text that will be changed by a pending autocorrection.
///
/// This method will only be called on iOS.
void showAutocorrectionPromptRect(int start, int end);
/// Platform notified framework of closed connection.
///
/// [TextInputClient] should cleanup its connection and finalize editing.
void connectionClosed();
/// The framework calls this method to notify that the text input control has
/// been changed.
///
/// The [TextInputClient] may switch to the new text input control by hiding
/// the old and showing the new input control.
///
/// See also:
///
/// * [TextInputControl.hide], a method to hide the old input control.
/// * [TextInputControl.show], a method to show the new input control.
void didChangeInputControl(TextInputControl? oldControl, TextInputControl? newControl) {}
/// Requests that the client show the editing toolbar, for example when the
/// platform changes the selection through a non-flutter method such as
/// scribble.
void showToolbar() {}
/// Requests that the client add a text placeholder to reserve visual space
/// in the text.
///
/// For example, this is called when responding to UIKit requesting
/// a text placeholder be added at the current selection, such as when
/// requesting additional writing space with iPadOS14 Scribble.
void insertTextPlaceholder(Size size) {}
/// Requests that the client remove the text placeholder.
void removeTextPlaceholder() {}
/// Performs the specified MacOS-specific selector from the
/// `NSStandardKeyBindingResponding` protocol or user-specified selector
/// from `DefaultKeyBinding.Dict`.
void performSelector(String selectorName) {}
}
/// An interface to receive focus from the engine.
///
/// This is currently only used to handle UIIndirectScribbleInteraction.
abstract class ScribbleClient {
/// A unique identifier for this element.
String get elementIdentifier;
/// Called by the engine when the [ScribbleClient] should receive focus.
///
/// For example, this method is called during a UIIndirectScribbleInteraction.
void onScribbleFocus(Offset offset);
/// Tests whether the [ScribbleClient] overlaps the given rectangle bounds.
bool isInScribbleRect(Rect rect);
/// The current bounds of the [ScribbleClient].
Rect get bounds;
}
/// Represents a selection rect for a character and it's position in the text.
///
/// This is used to report the current text selection rect and position data
/// to the engine for Scribble support on iPadOS 14.
@immutable
class SelectionRect {
/// Constructor for creating a [SelectionRect] from a text [position] and
/// [bounds].
const SelectionRect({
required this.position,
required this.bounds,
this.direction = TextDirection.ltr,
});
/// The position of this selection rect within the text String.
final int position;
/// The rectangle representing the bounds of this selection rect within the
/// currently focused [RenderEditable]'s coordinate space.
final Rect bounds;
/// The direction text flows within this selection rect.
final TextDirection direction;
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (runtimeType != other.runtimeType) {
return false;
}
return other is SelectionRect
&& other.position == position
&& other.bounds == bounds
&& other.direction == direction;
}
@override
int get hashCode => Object.hash(position, bounds);
@override
String toString() => 'SelectionRect($position, $bounds)';
}
/// An interface to receive granular information from [TextInput].
///
/// See also:
///
/// * [TextInput.attach]
/// * [TextInputConfiguration], to opt-in to receive [TextEditingDelta]'s from
/// the platforms [TextInput] you must set [TextInputConfiguration.enableDeltaModel]
/// to true.
mixin DeltaTextInputClient implements TextInputClient {
/// Requests that this client update its editing state by applying the deltas
/// received from the engine.
///
/// The list of [TextEditingDelta]'s are treated as changes that will be applied
/// to the client's editing state. A change is any mutation to the raw text
/// value, or any updates to the selection and/or composing region.
///
/// {@tool snippet}
/// This example shows what an implementation of this method could look like.
///
/// ```dart
/// class MyClient with DeltaTextInputClient {
/// TextEditingValue? _localValue;
///
/// @override
/// void updateEditingValueWithDeltas(List<TextEditingDelta> textEditingDeltas) {
/// if (_localValue == null) {
/// return;
/// }
/// TextEditingValue newValue = _localValue!;
/// for (final TextEditingDelta delta in textEditingDeltas) {
/// newValue = delta.apply(newValue);
/// }
/// _localValue = newValue;
/// }
///
/// // ...
/// }
/// ```
/// {@end-tool}
void updateEditingValueWithDeltas(List<TextEditingDelta> textEditingDeltas);
}
/// An interface for interacting with a text input control.
///
/// See also:
///
/// * [TextInput.attach], a method used to establish a [TextInputConnection]
/// between the system's text input and a [TextInputClient].
/// * [EditableText], a [TextInputClient] that connects to and interacts with
/// the system's text input using a [TextInputConnection].
class TextInputConnection {
TextInputConnection._(this._client)
: _id = _nextId++;
Size? _cachedSize;
Matrix4? _cachedTransform;
Rect? _cachedRect;
Rect? _cachedCaretRect;
List<SelectionRect> _cachedSelectionRects = <SelectionRect>[];
static int _nextId = 1;
final int _id;
/// Resets the internal ID counter for testing purposes.
///
/// This call has no effect when asserts are disabled. Calling it from
/// application code will likely break text input for the application.
@visibleForTesting
static void debugResetId({int to = 1}) {
assert(() {
_nextId = to;
return true;
}());
}
final TextInputClient _client;
/// Whether this connection is currently interacting with the text input control.
bool get attached => TextInput._instance._currentConnection == this;
/// Whether there is currently a Scribble interaction in progress.
///
/// This is used to make sure selection handles are shown when UIKit changes
/// the selection during a Scribble interaction.
bool get scribbleInProgress => TextInput._instance.scribbleInProgress;
/// Requests that the text input control become visible.
void show() {
assert(attached);
TextInput._instance._show();
}
/// Requests the system autofill UI to appear.
///
/// Currently only works on Android. Other platforms do not respond to this
/// message.
///
/// See also:
///
/// * [EditableText], a [TextInputClient] that calls this method when focused.
void requestAutofill() {
assert(attached);
TextInput._instance._requestAutofill();
}
/// Requests that the text input control update itself according to the new
/// [TextInputConfiguration].
void updateConfig(TextInputConfiguration configuration) {
assert(attached);
TextInput._instance._updateConfig(configuration);
}
/// Requests that the text input control change its internal state to match
/// the given state.
void setEditingState(TextEditingValue value) {
assert(attached);
TextInput._instance._setEditingState(value);
}
/// Send the size and transform of the editable text to engine.
///
/// The values are sent as platform messages so they can be used on web for
/// example to correctly position and size the html input field.
///
/// 1. [editableBoxSize]: size of the render editable box.
///
/// 2. [transform]: a matrix that maps the local paint coordinate system
/// to the [PipelineOwner.rootNode].
void setEditableSizeAndTransform(Size editableBoxSize, Matrix4 transform) {
if (editableBoxSize != _cachedSize || transform != _cachedTransform) {
_cachedSize = editableBoxSize;
_cachedTransform = transform;
TextInput._instance._setEditableSizeAndTransform(editableBoxSize, transform);
}
}
/// Send the smallest rect that covers the text in the client that's currently
/// being composed.
///
/// The given `rect` can not be null. If any of the 4 coordinates of the given
/// [Rect] is not finite, a [Rect] of size (-1, -1) will be sent instead.
///
/// This information is used for positioning the IME candidates menu on each
/// platform.
void setComposingRect(Rect rect) {
if (rect == _cachedRect) {
return;
}
_cachedRect = rect;
final Rect validRect = rect.isFinite ? rect : Offset.zero & const Size(-1, -1);
TextInput._instance._setComposingTextRect(validRect);
}
/// Sends the coordinates of caret rect. This is used on macOS for positioning
/// the accent selection menu.
void setCaretRect(Rect rect) {
if (rect == _cachedCaretRect) {
return;
}
_cachedCaretRect = rect;
final Rect validRect = rect.isFinite ? rect : Offset.zero & const Size(-1, -1);
TextInput._instance._setCaretRect(validRect);
}
/// Send the bounding boxes of the current selected glyphs in the client to
/// the platform's text input plugin.
///
/// These are used by the engine during a UIDirectScribbleInteraction.
void setSelectionRects(List<SelectionRect> selectionRects) {
if (!listEquals(_cachedSelectionRects, selectionRects)) {
_cachedSelectionRects = selectionRects;
TextInput._instance._setSelectionRects(selectionRects);
}
}
/// Send text styling information.
///
/// This information is used by the Flutter Web Engine to change the style
/// of the hidden native input's content. Hence, the content size will match
/// to the size of the editable widget's content.
void setStyle({
required String? fontFamily,
required double? fontSize,
required FontWeight? fontWeight,
required TextDirection textDirection,
required TextAlign textAlign,
}) {
assert(attached);
TextInput._instance._setStyle(
fontFamily: fontFamily,
fontSize: fontSize,
fontWeight: fontWeight,
textDirection: textDirection,
textAlign: textAlign,
);
}
/// Stop interacting with the text input control.
///
/// After calling this method, the text input control might disappear if no
/// other client attaches to it within this animation frame.
void close() {
if (attached) {
TextInput._instance._clearClient();
}
assert(!attached);
}
/// Platform sent a notification informing the connection is closed.
///
/// [TextInputConnection] should clean current client connection.
void connectionClosedReceived() {
TextInput._instance._currentConnection = null;
assert(!attached);
}
}
TextInputAction _toTextInputAction(String action) {
switch (action) {
case 'TextInputAction.none':
return TextInputAction.none;
case 'TextInputAction.unspecified':
return TextInputAction.unspecified;
case 'TextInputAction.go':
return TextInputAction.go;
case 'TextInputAction.search':
return TextInputAction.search;
case 'TextInputAction.send':
return TextInputAction.send;
case 'TextInputAction.next':
return TextInputAction.next;
case 'TextInputAction.previous':
return TextInputAction.previous;
case 'TextInputAction.continueAction':
return TextInputAction.continueAction;
case 'TextInputAction.join':
return TextInputAction.join;
case 'TextInputAction.route':
return TextInputAction.route;
case 'TextInputAction.emergencyCall':
return TextInputAction.emergencyCall;
case 'TextInputAction.done':
return TextInputAction.done;
case 'TextInputAction.newline':
return TextInputAction.newline;
}
throw FlutterError.fromParts(<DiagnosticsNode>[ErrorSummary('Unknown text input action: $action')]);
}
FloatingCursorDragState _toTextCursorAction(String state) {
switch (state) {
case 'FloatingCursorDragState.start':
return FloatingCursorDragState.Start;
case 'FloatingCursorDragState.update':
return FloatingCursorDragState.Update;
case 'FloatingCursorDragState.end':
return FloatingCursorDragState.End;
}
throw FlutterError.fromParts(<DiagnosticsNode>[ErrorSummary('Unknown text cursor action: $state')]);
}
RawFloatingCursorPoint _toTextPoint(FloatingCursorDragState state, Map<String, dynamic> encoded) {
assert(encoded['X'] != null, 'You must provide a value for the horizontal location of the floating cursor.');
assert(encoded['Y'] != null, 'You must provide a value for the vertical location of the floating cursor.');
final Offset offset = state == FloatingCursorDragState.Update
? Offset((encoded['X'] as num).toDouble(), (encoded['Y'] as num).toDouble())
: Offset.zero;
return RawFloatingCursorPoint(offset: offset, state: state);
}
/// An low-level interface to the system's text input control.
///
/// To start interacting with the system's text input control, call [attach] to
/// establish a [TextInputConnection] between the system's text input control
/// and a [TextInputClient]. The majority of commands available for
/// interacting with the text input control reside in the returned
/// [TextInputConnection]. The communication between the system text input and
/// the [TextInputClient] is asynchronous.
///
/// The platform text input plugin (which represents the system's text input)
/// and the [TextInputClient] usually maintain their own text editing states
/// ([TextEditingValue]) separately. They must be kept in sync as long as the
/// [TextInputClient] is connected. The following methods can be used to send
/// [TextEditingValue] to update the other party, when either party's text
/// editing states change:
///
/// * The [TextInput.attach] method allows a [TextInputClient] to establish a
/// connection to the text input. An optional field in its `configuration`
/// parameter can be used to specify an initial value for the platform text
/// input plugin's [TextEditingValue].
///
/// * The [TextInputClient] sends its [TextEditingValue] to the platform text
/// input plugin using [TextInputConnection.setEditingState].
///
/// * The platform text input plugin sends its [TextEditingValue] to the
/// connected [TextInputClient] via a "TextInput.setEditingState" message.
///
/// * When autofill happens on a disconnected [TextInputClient], the platform
/// text input plugin sends the [TextEditingValue] to the connected
/// [TextInputClient]'s [AutofillScope], and the [AutofillScope] will further
/// relay the value to the correct [TextInputClient].
///
/// When synchronizing the [TextEditingValue]s, the communication may get stuck
/// in an infinite when both parties are trying to send their own update. To
/// mitigate the problem, only [TextInputClient]s are allowed to alter the
/// received [TextEditingValue]s while platform text input plugins are to accept
/// the received [TextEditingValue]s unmodified. More specifically:
///
/// * When a [TextInputClient] receives a new [TextEditingValue] from the
/// platform text input plugin, it's allowed to modify the value (for example,
/// apply [TextInputFormatter]s). If it decides to do so, it must send the
/// updated [TextEditingValue] back to the platform text input plugin to keep
/// the [TextEditingValue]s in sync.
///
/// * When the platform text input plugin receives a new value from the
/// connected [TextInputClient], it must accept the new value as-is, to avoid
/// sending back an updated value.
///
/// See also:
///
/// * [TextField], a widget in which the user may enter text.
/// * [EditableText], a [TextInputClient] that connects to [TextInput] when it
/// wants to take user input from the keyboard.
class TextInput {
TextInput._() {
_channel = SystemChannels.textInput;
_channel.setMethodCallHandler(_loudlyHandleTextInputInvocation);
}
/// Set the [MethodChannel] used to communicate with the system's text input
/// control.
///
/// This is only meant for testing within the Flutter SDK. Changing this
/// will break the ability to input text. This has no effect if asserts are
/// disabled.
@visibleForTesting
static void setChannel(MethodChannel newChannel) {
assert(() {
_instance._channel = newChannel..setMethodCallHandler(_instance._loudlyHandleTextInputInvocation);
return true;
}());
}
static final TextInput _instance = TextInput._();
static void _addInputControl(TextInputControl control) {
if (control != _PlatformTextInputControl.instance) {
_instance._inputControls.add(control);
}
}
static void _removeInputControl(TextInputControl control) {
if (control != _PlatformTextInputControl.instance) {
_instance._inputControls.remove(control);
}
}
/// Sets the current text input control.
///
/// The current text input control receives text input state changes and visual
/// text input control requests, such as showing and hiding the input control,
/// from the framework.
///
/// Setting the current text input control as `null` removes the visual text
/// input control.
///
/// See also:
///
/// * [TextInputControl], an interface for implementing text input controls.
/// * [TextInput.restorePlatformInputControl], a method to restore the default
/// platform text input control.
static void setInputControl(TextInputControl? newControl) {
final TextInputControl? oldControl = _instance._currentControl;
if (newControl == oldControl) {
return;
}
if (newControl != null) {
_addInputControl(newControl);
}
if (oldControl != null) {
_removeInputControl(oldControl);
}
_instance._currentControl = newControl;
final TextInputClient? client = _instance._currentConnection?._client;
client?.didChangeInputControl(oldControl, newControl);
}
/// Restores the default platform text input control.
///
/// See also:
///
/// * [TextInput.setInputControl], a method to set a custom input
/// control, or to remove the visual input control.
static void restorePlatformInputControl() {
setInputControl(_PlatformTextInputControl.instance);
}
TextInputControl? _currentControl = _PlatformTextInputControl.instance;
final Set<TextInputControl> _inputControls = <TextInputControl>{
_PlatformTextInputControl.instance,
};
static const List<TextInputAction> _androidSupportedInputActions = <TextInputAction>[
TextInputAction.none,
TextInputAction.unspecified,
TextInputAction.done,
TextInputAction.send,
TextInputAction.go,
TextInputAction.search,
TextInputAction.next,
TextInputAction.previous,
TextInputAction.newline,
];
static const List<TextInputAction> _iOSSupportedInputActions = <TextInputAction>[
TextInputAction.unspecified,
TextInputAction.done,
TextInputAction.send,
TextInputAction.go,
TextInputAction.search,
TextInputAction.next,
TextInputAction.newline,
TextInputAction.continueAction,
TextInputAction.join,
TextInputAction.route,
TextInputAction.emergencyCall,
];
/// Ensure that a [TextInput] instance has been set up so that the platform
/// can handle messages on the text input method channel.
static void ensureInitialized() {
_instance; // ignore: unnecessary_statements
}
/// Begin interacting with the text input control.
///
/// Calling this function helps multiple clients coordinate about which one is
/// currently interacting with the text input control. The returned
/// [TextInputConnection] provides an interface for actually interacting with
/// the text input control.
///
/// A client that no longer wishes to interact with the text input control
/// should call [TextInputConnection.close] on the returned
/// [TextInputConnection].
static TextInputConnection attach(TextInputClient client, TextInputConfiguration configuration) {
final TextInputConnection connection = TextInputConnection._(client);
_instance._attach(connection, configuration);
return connection;
}
// This method actually notifies the embedding of the client. It is utilized
// by [attach] and by [_handleTextInputInvocation] for the
// `TextInputClient.requestExistingInputState` method.
void _attach(TextInputConnection connection, TextInputConfiguration configuration) {
assert(_debugEnsureInputActionWorksOnPlatform(configuration.inputAction));
_currentConnection = connection;
_currentConfiguration = configuration;
_setClient(connection._client, configuration);
}
static bool _debugEnsureInputActionWorksOnPlatform(TextInputAction inputAction) {
assert(() {
if (kIsWeb) {
// TODO(flutterweb): what makes sense here?
return true;
}
if (Platform.isIOS) {
assert(
_iOSSupportedInputActions.contains(inputAction),
'The requested TextInputAction "$inputAction" is not supported on iOS.',
);
} else if (Platform.isAndroid) {
assert(
_androidSupportedInputActions.contains(inputAction),
'The requested TextInputAction "$inputAction" is not supported on Android.',
);
}
return true;
}());
return true;
}
late MethodChannel _channel;
TextInputConnection? _currentConnection;
late TextInputConfiguration _currentConfiguration;
final Map<String, ScribbleClient> _scribbleClients = <String, ScribbleClient>{};
bool _scribbleInProgress = false;
/// Used for testing within the Flutter SDK to get the currently registered [ScribbleClient] list.
@visibleForTesting
static Map<String, ScribbleClient> get scribbleClients => TextInput._instance._scribbleClients;
/// Returns true if a scribble interaction is currently happening.
bool get scribbleInProgress => _scribbleInProgress;
Future<dynamic> _loudlyHandleTextInputInvocation(MethodCall call) async {
try {
return await _handleTextInputInvocation(call);
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'services library',
context: ErrorDescription('during method call ${call.method}'),
informationCollector: () => <DiagnosticsNode>[
DiagnosticsProperty<MethodCall>('call', call, style: DiagnosticsTreeStyle.errorProperty),
],
));
rethrow;
}
}
Future<dynamic> _handleTextInputInvocation(MethodCall methodCall) async {
final String method = methodCall.method;
if (method == 'TextInputClient.focusElement') {
final List<dynamic> args = methodCall.arguments as List<dynamic>;
_scribbleClients[args[0]]?.onScribbleFocus(Offset((args[1] as num).toDouble(), (args[2] as num).toDouble()));
return;
} else if (method == 'TextInputClient.requestElementsInRect') {
final List<double> args = (methodCall.arguments as List<dynamic>).cast<num>().map<double>((num value) => value.toDouble()).toList();
return _scribbleClients.keys.where((String elementIdentifier) {
final Rect rect = Rect.fromLTWH(args[0], args[1], args[2], args[3]);
if (!(_scribbleClients[elementIdentifier]?.isInScribbleRect(rect) ?? false)) {
return false;
}
final Rect bounds = _scribbleClients[elementIdentifier]?.bounds ?? Rect.zero;
return !(bounds == Rect.zero || bounds.hasNaN || bounds.isInfinite);
}).map((String elementIdentifier) {
final Rect bounds = _scribbleClients[elementIdentifier]!.bounds;
return <dynamic>[elementIdentifier, ...<dynamic>[bounds.left, bounds.top, bounds.width, bounds.height]];
}).toList();
} else if (method == 'TextInputClient.scribbleInteractionBegan') {
_scribbleInProgress = true;
return;
} else if (method == 'TextInputClient.scribbleInteractionFinished') {
_scribbleInProgress = false;
return;
}
if (_currentConnection == null) {
return;
}
// The requestExistingInputState request needs to be handled regardless of
// the client ID, as long as we have a _currentConnection.
if (method == 'TextInputClient.requestExistingInputState') {
_attach(_currentConnection!, _currentConfiguration);
final TextEditingValue? editingValue = _currentConnection!._client.currentTextEditingValue;
if (editingValue != null) {
_setEditingState(editingValue);
}
return;
}
final List<dynamic> args = methodCall.arguments as List<dynamic>;
// The updateEditingStateWithTag request (autofill) can come up even to a
// text field that doesn't have a connection.
if (method == 'TextInputClient.updateEditingStateWithTag') {
final TextInputClient client = _currentConnection!._client;
final AutofillScope? scope = client.currentAutofillScope;
final Map<String, dynamic> editingValue = args[1] as Map<String, dynamic>;
for (final String tag in editingValue.keys) {
final TextEditingValue textEditingValue = TextEditingValue.fromJSON(
editingValue[tag] as Map<String, dynamic>,
);
final AutofillClient? client = scope?.getAutofillClient(tag);
if (client != null && client.textInputConfiguration.autofillConfiguration.enabled) {
client.autofill(textEditingValue);
}
}
return;
}
final int client = args[0] as int;
if (client != _currentConnection!._id) {
// If the client IDs don't match, the incoming message was for a different
// client.
bool debugAllowAnyway = false;
assert(() {
// In debug builds we allow "-1" as a magical client ID that ignores
// this verification step so that tests can always get through, even
// when they are not mocking the engine side of text input.
if (client == -1) {
debugAllowAnyway = true;
}
return true;
}());
if (!debugAllowAnyway) {
return;
}
}
switch (method) {
case 'TextInputClient.updateEditingState':
final TextEditingValue value = TextEditingValue.fromJSON(args[1] as Map<String, dynamic>);
TextInput._instance._updateEditingValue(value, exclude: _PlatformTextInputControl.instance);
break;
case 'TextInputClient.updateEditingStateWithDeltas':
assert(_currentConnection!._client is DeltaTextInputClient, 'You must be using a DeltaTextInputClient if TextInputConfiguration.enableDeltaModel is set to true');
final List<TextEditingDelta> deltas = <TextEditingDelta>[];
final Map<String, dynamic> encoded = args[1] as Map<String, dynamic>;
for (final dynamic encodedDelta in encoded['deltas'] as List<dynamic>) {
final TextEditingDelta delta = TextEditingDelta.fromJSON(encodedDelta as Map<String, dynamic>);
deltas.add(delta);
}
(_currentConnection!._client as DeltaTextInputClient).updateEditingValueWithDeltas(deltas);
break;
case 'TextInputClient.performAction':
_currentConnection!._client.performAction(_toTextInputAction(args[1] as String));
break;
case 'TextInputClient.performSelectors':
final List<String> selectors = (args[1] as List<dynamic>).cast<String>();
selectors.forEach(_currentConnection!._client.performSelector);
break;
case 'TextInputClient.performPrivateCommand':
final Map<String, dynamic> firstArg = args[1] as Map<String, dynamic>;
_currentConnection!._client.performPrivateCommand(
firstArg['action'] as String,
firstArg['data'] == null
? <String, dynamic>{}
: firstArg['data'] as Map<String, dynamic>,
);
break;
case 'TextInputClient.updateFloatingCursor':
_currentConnection!._client.updateFloatingCursor(_toTextPoint(
_toTextCursorAction(args[1] as String),
args[2] as Map<String, dynamic>,
));
break;
case 'TextInputClient.onConnectionClosed':
_currentConnection!._client.connectionClosed();
break;
case 'TextInputClient.showAutocorrectionPromptRect':
_currentConnection!._client.showAutocorrectionPromptRect(args[1] as int, args[2] as int);
break;
case 'TextInputClient.showToolbar':
_currentConnection!._client.showToolbar();
break;
case 'TextInputClient.insertTextPlaceholder':
_currentConnection!._client.insertTextPlaceholder(Size((args[1] as num).toDouble(), (args[2] as num).toDouble()));
break;
case 'TextInputClient.removeTextPlaceholder':
_currentConnection!._client.removeTextPlaceholder();
break;
default:
throw MissingPluginException();
}
}
bool _hidePending = false;
void _scheduleHide() {
if (_hidePending) {
return;
}
_hidePending = true;
// Schedule a deferred task that hides the text input. If someone else
// shows the keyboard during this update cycle, then the task will do
// nothing.
scheduleMicrotask(() {
_hidePending = false;
if (_currentConnection == null) {
_hide();
}
});
}
void _setClient(TextInputClient client, TextInputConfiguration configuration) {
for (final TextInputControl control in _inputControls) {
control.attach(client, configuration);
}
}
void _clearClient() {
final TextInputClient client = _currentConnection!._client;
for (final TextInputControl control in _inputControls) {
control.detach(client);
}
_currentConnection = null;
_scheduleHide();
}
void _updateConfig(TextInputConfiguration configuration) {
for (final TextInputControl control in _inputControls) {
control.updateConfig(configuration);
}
}
void _setEditingState(TextEditingValue value) {
for (final TextInputControl control in _inputControls) {
control.setEditingState(value);
}
}
void _show() {
for (final TextInputControl control in _inputControls) {
control.show();
}
}
void _hide() {
for (final TextInputControl control in _inputControls) {
control.hide();
}
}
void _setEditableSizeAndTransform(Size editableBoxSize, Matrix4 transform) {
for (final TextInputControl control in _inputControls) {
control.setEditableSizeAndTransform(editableBoxSize, transform);
}
}
void _setComposingTextRect(Rect rect) {
for (final TextInputControl control in _inputControls) {
control.setComposingRect(rect);
}
}
void _setCaretRect(Rect rect) {
for (final TextInputControl control in _inputControls) {
control.setCaretRect(rect);
}
}
void _setSelectionRects(List<SelectionRect> selectionRects) {
for (final TextInputControl control in _inputControls) {
control.setSelectionRects(selectionRects);
}
}
void _setStyle({
required String? fontFamily,
required double? fontSize,
required FontWeight? fontWeight,
required TextDirection textDirection,
required TextAlign textAlign,
}) {
for (final TextInputControl control in _inputControls) {
control.setStyle(
fontFamily: fontFamily,
fontSize: fontSize,
fontWeight: fontWeight,
textDirection: textDirection,
textAlign: textAlign,
);
}
}
void _requestAutofill() {
for (final TextInputControl control in _inputControls) {
control.requestAutofill();
}
}
void _updateEditingValue(TextEditingValue value, {TextInputControl? exclude}) {
if (_currentConnection == null) {
return;
}
for (final TextInputControl control in _instance._inputControls) {
if (control != exclude) {
control.setEditingState(value);
}
}
_instance._currentConnection!._client.updateEditingValue(value);
}
/// Updates the editing value of the attached input client.
///
/// This method should be called by the text input control implementation to
/// send editing value updates to the attached input client.
static void updateEditingValue(TextEditingValue value) {
_instance._updateEditingValue(value, exclude: _instance._currentControl);
}
/// Finishes the current autofill context, and potentially saves the user
/// input for future use if `shouldSave` is true.
///
/// Typically, this method should be called when the user has finalized their
/// input. For example, in a [Form], it's typically done immediately before or
/// after its content is submitted.
///
/// The topmost [AutofillGroup]s also call [finishAutofillContext]
/// automatically when they are disposed. The default behavior can be
/// overridden in [AutofillGroup.onDisposeAction].
///
/// {@template flutter.services.TextInput.finishAutofillContext}
/// An autofill context is a collection of input fields that live in the
/// platform's text input plugin. The platform is encouraged to save the user
/// input stored in the current autofill context before the context is
/// destroyed, when [TextInput.finishAutofillContext] is called with
/// `shouldSave` set to true.
///
/// Currently, there can only be at most one autofill context at any given
/// time. When any input field in an [AutofillGroup] requests for autofill
/// (which is done automatically when an autofillable [EditableText] gains
/// focus), the current autofill context will merge the content of that
/// [AutofillGroup] into itself. When there isn't an existing autofill context,
/// one will be created to hold the newly added input fields from the group.
///
/// Once added to an autofill context, an input field will stay in the context
/// until the context is destroyed. To prevent leaks, call
/// [TextInput.finishAutofillContext] to signal the text input plugin that the
/// user has finalized their input in the current autofill context. The
/// platform text input plugin either encourages or discourages the platform
/// from saving the user input based on the value of the `shouldSave`
/// parameter. The platform usually shows a "Save for autofill?" prompt for
/// user confirmation.
/// {@endtemplate}
///
/// On many platforms, calling [finishAutofillContext] shows the save user
/// input dialog and disrupts the user's flow. Ideally the dialog should only
/// be shown no more than once for every screen. Consider removing premature
/// [finishAutofillContext] calls to prevent showing the save user input UI
/// too frequently. However, calling [finishAutofillContext] when there's no
/// existing autofill context usually does not bring up the save user input
/// UI.
///
/// See also:
///
/// * [EditableText.autofillHints] for autofill save troubleshooting tips.
/// * [AutofillGroup.onDisposeAction], a configurable action that runs when a
/// topmost [AutofillGroup] is getting disposed.
static void finishAutofillContext({ bool shouldSave = true }) {
for (final TextInputControl control in TextInput._instance._inputControls) {
control.finishAutofillContext(shouldSave: shouldSave);
}
}
/// Registers a [ScribbleClient] with [elementIdentifier] that can be focused
/// by the engine.
///
/// For example, the registered [ScribbleClient] list is used to respond to
/// UIIndirectScribbleInteraction on an iPad.
static void registerScribbleElement(String elementIdentifier, ScribbleClient scribbleClient) {
TextInput._instance._scribbleClients[elementIdentifier] = scribbleClient;
}
/// Unregisters a [ScribbleClient] with [elementIdentifier].
static void unregisterScribbleElement(String elementIdentifier) {
TextInput._instance._scribbleClients.remove(elementIdentifier);
}
}
/// An interface for implementing text input controls that receive text editing
/// state changes and visual input control requests.
///
/// Editing state changes and input control requests are sent by the framework
/// when the editing state of the attached text input client changes, or it
/// requests the input control to be shown or hidden, for example.
///
/// The input control can be installed with [TextInput.setInputControl], and the
/// default platform text input control can be restored with
/// [TextInput.restorePlatformInputControl].
///
/// The [TextInputControl] class must be extended. [TextInputControl]
/// implementations should call [TextInput.updateEditingValue] to send user
/// input to the attached input client.
///
/// {@tool dartpad}
/// This example illustrates a basic [TextInputControl] implementation.
///
/// ** See code in examples/api/lib/services/text_input/text_input_control.0.dart **
/// {@end-tool}
///
/// See also:
///
/// * [TextInput.setInputControl], a method to install a custom text input control.
/// * [TextInput.restorePlatformInputControl], a method to restore the default
/// platform text input control.
/// * [TextInput.updateEditingValue], a method to send user input to
/// the framework.
mixin TextInputControl {
/// Requests the text input control to attach to the given input client.
///
/// This method is called when a text input client is attached. The input
/// control should update its configuration to match the client's configuration.
void attach(TextInputClient client, TextInputConfiguration configuration) {}
/// Requests the text input control to detach from the given input client.
///
/// This method is called when a text input client is detached. The input
/// control should release any resources allocated for the client.
void detach(TextInputClient client) {}
/// Requests that the text input control is shown.
///
/// This method is called when the input control should become visible.
void show() {}
/// Requests that the text input control is hidden.
///
/// This method is called when the input control should hide.
void hide() {}
/// Informs the text input control about input configuration changes.
///
/// This method is called when the configuration of the attached input client
/// has changed.
void updateConfig(TextInputConfiguration configuration) {}
/// Informs the text input control about editing state changes.
///
/// This method is called when the editing state of the attached input client
/// has changed.
void setEditingState(TextEditingValue value) {}
/// Informs the text input control about client position changes.
///
/// This method is called on when the input control should position itself in
/// relation to the attached input client.
void setEditableSizeAndTransform(Size editableBoxSize, Matrix4 transform) {}
/// Informs the text input control about composing area changes.
///
/// This method is called when the attached input client's composing area
/// changes.
void setComposingRect(Rect rect) {}
/// Informs the text input control about caret area changes.
///
/// This method is called when the attached input client's caret area
/// changes.
void setCaretRect(Rect rect) {}
/// Informs the text input control about selection area changes.
///
/// This method is called when the attached input client's selection area
/// changes.
void setSelectionRects(List<SelectionRect> selectionRects) {}
/// Informs the text input control about text style changes.
///
/// This method is called on the when the attached input client's text style
/// changes.
void setStyle({
required String? fontFamily,
required double? fontSize,
required FontWeight? fontWeight,
required TextDirection textDirection,
required TextAlign textAlign,
}) {}
/// Requests autofill from the text input control.
///
/// This method is called when the autofill UI should appear.
void requestAutofill() {}
/// Requests that the autofill context is finalized.
///
/// See also:
///
/// * [TextInput.finishAutofillContext]
void finishAutofillContext({bool shouldSave = true}) {}
}
/// Provides access to the platform text input control.
class _PlatformTextInputControl with TextInputControl {
_PlatformTextInputControl._();
/// The shared instance of [_PlatformTextInputControl].
static final _PlatformTextInputControl instance = _PlatformTextInputControl._();
MethodChannel get _channel => TextInput._instance._channel;
Map<String, dynamic> _configurationToJson(TextInputConfiguration configuration) {
final Map<String, dynamic> json = configuration.toJson();
if (TextInput._instance._currentControl != _PlatformTextInputControl.instance) {
json['inputType'] = TextInputType.none.toJson();
}
return json;
}
@override
void attach(TextInputClient client, TextInputConfiguration configuration) {
_channel.invokeMethod<void>(
'TextInput.setClient',
<Object>[
TextInput._instance._currentConnection!._id,
_configurationToJson(configuration),
],
);
}
@override
void detach(TextInputClient client) {
_channel.invokeMethod<void>('TextInput.clearClient');
}
@override
void updateConfig(TextInputConfiguration configuration) {
_channel.invokeMethod<void>(
'TextInput.updateConfig',
_configurationToJson(configuration),
);
}
@override
void setEditingState(TextEditingValue value) {
_channel.invokeMethod<void>(
'TextInput.setEditingState',
value.toJSON(),
);
}
@override
void show() {
_channel.invokeMethod<void>('TextInput.show');
}
@override
void hide() {
_channel.invokeMethod<void>('TextInput.hide');
}
@override
void setEditableSizeAndTransform(Size editableBoxSize, Matrix4 transform) {
_channel.invokeMethod<void>(
'TextInput.setEditableSizeAndTransform',
<String, dynamic>{
'width': editableBoxSize.width,
'height': editableBoxSize.height,
'transform': transform.storage,
},
);
}
@override
void setComposingRect(Rect rect) {
_channel.invokeMethod<void>(
'TextInput.setMarkedTextRect',
<String, dynamic>{
'width': rect.width,
'height': rect.height,
'x': rect.left,
'y': rect.top,
},
);
}
@override
void setCaretRect(Rect rect) {
_channel.invokeMethod<void>(
'TextInput.setCaretRect',
<String, dynamic>{
'width': rect.width,
'height': rect.height,
'x': rect.left,
'y': rect.top,
},
);
}
@override
void setSelectionRects(List<SelectionRect> selectionRects) {
_channel.invokeMethod<void>(
'TextInput.setSelectionRects',
selectionRects.map((SelectionRect rect) {
return <num>[
rect.bounds.left,
rect.bounds.top,
rect.bounds.width,
rect.bounds.height,
rect.position,
rect.direction.index,
];
}).toList(),
);
}
@override
void setStyle({
required String? fontFamily,
required double? fontSize,
required FontWeight? fontWeight,
required TextDirection textDirection,
required TextAlign textAlign,
}) {
_channel.invokeMethod<void>(
'TextInput.setStyle',
<String, dynamic>{
'fontFamily': fontFamily,
'fontSize': fontSize,
'fontWeightIndex': fontWeight?.index,
'textAlignIndex': textAlign.index,
'textDirectionIndex': textDirection.index,
},
);
}
@override
void requestAutofill() {
_channel.invokeMethod<void>('TextInput.requestAutofill');
}
@override
void finishAutofillContext({bool shouldSave = true}) {
_channel.invokeMethod<void>(
'TextInput.finishAutofillContext',
shouldSave,
);
}
}