Skip to main content

inset_embedder/
text_input.rs

1//! Flutter counterpart: `services/text_input.dart` (value types and
2//! [`TextInputConfiguration`]).
3//!
4//! Lives here because [`crate::View`] and [`crate::EmbedderClient`] name these
5//! types. The connection and client stay in `inset-services`.
6
7use std::fmt;
8
9use crate::text_editing::TextSelection;
10use crate::{Brightness, Locale, Offset, TextAffinity, TextPosition, TextRange};
11
12/// Indicates how to handle the intelligent replacement of dashes in text input.
13///
14/// See also:
15///
16///  * `TextField.smartDashesType`
17///  * `CupertinoTextField.smartDashesType`
18///  * `EditableText.smartDashesType`
19///  * [`SmartQuotesType`]
20///  * <https://developer.apple.com/documentation/uikit/uitextinputtraits>
21#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
22pub enum SmartDashesType {
23    /// Smart dashes is disabled.
24    ///
25    /// This corresponds to the
26    /// ["no" value of UITextSmartDashesType](https://developer.apple.com/documentation/uikit/uitextsmartdashestype/no).
27    Disabled,
28
29    /// Smart dashes is enabled.
30    ///
31    /// This corresponds to the
32    /// ["yes" value of UITextSmartDashesType](https://developer.apple.com/documentation/uikit/uitextsmartdashestype/yes).
33    Enabled,
34}
35
36/// Indicates how to handle the intelligent replacement of quotes in text input.
37///
38/// See also:
39///
40///  * `TextField.smartQuotesType`
41///  * `CupertinoTextField.smartQuotesType`
42///  * `EditableText.smartQuotesType`
43///  * <https://developer.apple.com/documentation/uikit/uitextinputtraits>
44#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
45pub enum SmartQuotesType {
46    /// Smart quotes is disabled.
47    ///
48    /// This corresponds to the
49    /// ["no" value of UITextSmartQuotesType](https://developer.apple.com/documentation/uikit/uitextsmartquotestype/no).
50    Disabled,
51
52    /// Smart quotes is enabled.
53    ///
54    /// This corresponds to the
55    /// ["yes" value of UITextSmartQuotesType](https://developer.apple.com/documentation/uikit/uitextsmartquotestype/yes).
56    Enabled,
57}
58
59/// The type of information for which to optimize the text input control.
60///
61/// On Android, behavior may vary across device and keyboard provider.
62///
63/// This class stays as close to `Enum` interface as possible, and allows
64/// for additional flags for some input types. For example, numeric input
65/// can specify whether it supports decimal numbers and/or signed numbers.
66#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
67pub struct TextInputType {
68    /// Enum value index, corresponds to one of the [`VALUES`](Self::VALUES).
69    pub index: i32,
70    /// The number is signed, allowing a positive or negative sign at the start.
71    ///
72    /// This flag is only used for the [`NUMBER`](Self::NUMBER) input type, otherwise `None`.
73    /// Use `TextInputType::number_with_options().signed(true)` to set this.
74    pub signed: Option<bool>,
75    /// The number is decimal, allowing a decimal point to provide fractional.
76    ///
77    /// This flag is only used for the [`NUMBER`](Self::NUMBER) input type, otherwise `None`.
78    /// Use `TextInputType::number_with_options().decimal(true)` to set this.
79    pub decimal: Option<bool>,
80    /// The number is a password.
81    ///
82    /// This flag is only used for the [`NUMBER`](Self::NUMBER) input type, otherwise `None`.
83    /// Use `TextInputType::number_with_options().password(true)` to set this.
84    ///
85    /// On Android, this adds
86    /// [TYPE_NUMBER_VARIATION_PASSWORD](https://developer.android.com/reference/android/text/InputType#TYPE_NUMBER_VARIATION_PASSWORD)
87    /// to the input type. It has no effect on iOS, web, or desktop platforms.
88    pub password: Option<bool>,
89}
90
91impl TextInputType {
92    const fn named(index: i32) -> TextInputType {
93        TextInputType {
94            index,
95            signed: None,
96            decimal: None,
97            password: None,
98        }
99    }
100
101    /// Optimize for textual information.
102    ///
103    /// Requests the default platform keyboard.
104    pub const TEXT: TextInputType = TextInputType::named(0);
105
106    /// Optimize for multiline textual information.
107    ///
108    /// Requests the default platform keyboard, but accepts newlines when the
109    /// enter key is pressed. This is the input type used for all multiline text
110    /// fields.
111    pub const MULTILINE: TextInputType = TextInputType::named(1);
112
113    /// Optimize for unsigned numerical information without a decimal point.
114    ///
115    /// Requests a default keyboard with ready access to the number keys.
116    /// Additional options, such as decimal point and/or positive/negative
117    /// signs, can be requested using [`number_with_options`](Self::number_with_options).
118    pub const NUMBER: TextInputType = TextInputType::number_with_options();
119
120    /// Optimize for telephone numbers.
121    ///
122    /// Requests a keyboard with ready access to the number keys, "*", and "#".
123    pub const PHONE: TextInputType = TextInputType::named(3);
124
125    /// Optimize for date and time information.
126    ///
127    /// On iOS, requests the default keyboard.
128    ///
129    /// On Android, requests a keyboard with ready access to the number keys,
130    /// ":", and "-".
131    pub const DATETIME: TextInputType = TextInputType::named(4);
132
133    /// Optimize for email addresses.
134    ///
135    /// Requests a keyboard with ready access to the "@" and "." keys.
136    pub const EMAIL_ADDRESS: TextInputType = TextInputType::named(5);
137
138    /// Optimize for URLs.
139    ///
140    /// Requests a keyboard with ready access to the "/" and "." keys.
141    pub const URL: TextInputType = TextInputType::named(6);
142
143    /// Optimize for passwords that are visible to the user.
144    ///
145    /// Requests a keyboard with ready access to both letters and numbers.
146    pub const VISIBLE_PASSWORD: TextInputType = TextInputType::named(7);
147
148    /// Optimized for a person's name.
149    ///
150    /// On iOS, requests the
151    /// [UIKeyboardType.namePhonePad](https://developer.apple.com/documentation/uikit/uikeyboardtype/namephonepad)
152    /// keyboard, a keyboard optimized for entering a person’s name or phone number.
153    /// Does not support auto-capitalization.
154    ///
155    /// On Android, requests a keyboard optimized for
156    /// [TYPE_TEXT_VARIATION_PERSON_NAME](https://developer.android.com/reference/android/text/InputType#TYPE_TEXT_VARIATION_PERSON_NAME).
157    pub const NAME: TextInputType = TextInputType::named(8);
158
159    /// Optimized for postal mailing addresses.
160    ///
161    /// On iOS, requests the default keyboard.
162    ///
163    /// On Android, requests a keyboard optimized for
164    /// [TYPE_TEXT_VARIATION_POSTAL_ADDRESS](https://developer.android.com/reference/android/text/InputType#TYPE_TEXT_VARIATION_POSTAL_ADDRESS).
165    pub const STREET_ADDRESS: TextInputType = TextInputType::named(9);
166
167    /// Prevent the OS from showing the on-screen virtual keyboard.
168    pub const NONE: TextInputType = TextInputType::named(10);
169
170    /// Optimized for web searches.
171    ///
172    /// Requests a keyboard that includes keys useful for web searches as well as URLs.
173    ///
174    /// On iOS, requests a default keyboard with ready access to the "." key. In contrast to
175    /// [`URL`](Self::URL), a space bar is available.
176    ///
177    /// On Android this is remapped to the [`URL`](Self::URL) keyboard type as it always shows a space bar.
178    pub const WEB_SEARCH: TextInputType = TextInputType::named(11);
179
180    /// Optimized for social media.
181    ///
182    /// Requests a keyboard that includes keys useful for handles and tags.
183    ///
184    /// On iOS, requests a default keyboard with ready access to the "@" and "#" keys.
185    ///
186    /// On Android this is remapped to the [`EMAIL_ADDRESS`](Self::EMAIL_ADDRESS) keyboard type as it always shows the "@" key.
187    pub const TWITTER: TextInputType = TextInputType::named(12);
188
189    /// All possible enum values.
190    pub const VALUES: &[TextInputType] = &[
191        Self::TEXT,
192        Self::MULTILINE,
193        Self::NUMBER,
194        Self::PHONE,
195        Self::DATETIME,
196        Self::EMAIL_ADDRESS,
197        Self::URL,
198        Self::VISIBLE_PASSWORD,
199        Self::NAME,
200        Self::STREET_ADDRESS,
201        Self::NONE,
202        Self::WEB_SEARCH,
203        Self::TWITTER,
204    ];
205
206    const NAMES: &[&str] = &[
207        "text",
208        "multiline",
209        "number",
210        "phone",
211        "datetime",
212        "emailAddress",
213        "url",
214        "visiblePassword",
215        "name",
216        "address",
217        "none",
218        "webSearch",
219        "twitter",
220    ];
221
222    /// Optimize for numerical information.
223    ///
224    /// Requests a numeric keyboard with additional settings.
225    /// The [`signed`](Self::signed), [`decimal`](Self::decimal), and [`password`](Self::password)
226    /// parameters are optional.
227    pub const fn number_with_options() -> TextInputType {
228        TextInputType {
229            index: 2,
230            signed: Some(false),
231            decimal: Some(false),
232            password: Some(false),
233        }
234    }
235
236    /// Dart `TextInputType.numberWithOptions(signed:)`.
237    pub fn signed(mut self, signed: bool) -> TextInputType {
238        self.signed = Some(signed);
239        self
240    }
241
242    /// Dart `TextInputType.numberWithOptions(decimal:)`.
243    pub fn decimal(mut self, decimal: bool) -> TextInputType {
244        self.decimal = Some(decimal);
245        self
246    }
247
248    /// Dart `TextInputType.numberWithOptions(password:)`.
249    pub fn password(mut self, password: bool) -> TextInputType {
250        self.password = Some(password);
251        self
252    }
253
254    fn name(self) -> String {
255        format!("TextInputType.{}", Self::NAMES[self.index as usize])
256    }
257}
258
259impl fmt::Display for TextInputType {
260    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261        write!(
262            f,
263            "TextInputType(name: {}, signed: {:?}, decimal: {:?}, password: {:?})",
264            self.name(),
265            self.signed,
266            self.decimal,
267            self.password
268        )
269    }
270}
271
272/// An action the user has requested the text input control to perform.
273///
274/// Each action represents a logical meaning, and also configures the soft
275/// keyboard to display a certain kind of action button. The visual appearance
276/// of the action button might differ between versions of the same OS.
277///
278/// Despite the logical meaning of each action, choosing a particular
279/// [`TextInputAction`] does not necessarily cause any specific behavior to
280/// happen, other than changing the focus when appropriate. It is up to the
281/// developer to ensure that the behavior that occurs when an action button is
282/// pressed is appropriate for the action button chosen.
283///
284/// For example: If the user presses the keyboard action button on iOS when it
285/// reads "Emergency Call", the result should not be a focus change to the next
286/// TextField. This behavior is not logically appropriate for a button that says
287/// "Emergency Call".
288///
289/// See `EditableText` for more information about customizing action button
290/// behavior.
291///
292/// Most [`TextInputAction`]s are supported equally by both Android and iOS.
293/// However, there is not a complete, direct mapping between Android's IME input
294/// types and iOS's keyboard return types. Therefore, some [`TextInputAction`]s
295/// are inappropriate for one of the platforms. If a developer chooses an
296/// inappropriate [`TextInputAction`] when running in debug mode, an error will be
297/// thrown. If the same thing is done in release mode, then instead of sending
298/// the inappropriate value, Android will use "unspecified" on the platform
299/// side and iOS will use "default" on the platform side.
300///
301/// See also:
302///
303///  * `TextInput`, which configures the platform's keyboard setup.
304///  * `EditableText`, which invokes callbacks when the action button is pressed.
305//
306// This class has been cloned to `flutter_driver/lib/src/common/action.dart` as `TextInputAction`,
307// and must be kept in sync.
308#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
309pub enum TextInputAction {
310    /// Logical meaning: There is no relevant input action for the current input
311    /// source, e.g., `TextField`.
312    ///
313    /// Android: Corresponds to Android's "IME_ACTION_NONE". The keyboard setup
314    /// is decided by the OS. The keyboard will likely show a return key.
315    ///
316    /// iOS: iOS does not have a keyboard return type of "none." It is
317    /// inappropriate to choose this [`TextInputAction`] when running on iOS.
318    None,
319
320    /// Logical meaning: Let the OS decide which action is most appropriate.
321    ///
322    /// Android: Corresponds to Android's "IME_ACTION_UNSPECIFIED". The OS chooses
323    /// which keyboard action to display. The decision will likely be a done
324    /// button or a return key.
325    ///
326    /// iOS: Corresponds to iOS's "UIReturnKeyDefault". The title displayed in
327    /// the action button is "return".
328    Unspecified,
329
330    /// Logical meaning: The user is done providing input to a group of inputs
331    /// (like a form). Some kind of finalization behavior should now take place.
332    ///
333    /// Android: Corresponds to Android's "IME_ACTION_DONE". The OS displays a
334    /// button that represents completion, e.g., a checkmark button.
335    ///
336    /// iOS: Corresponds to iOS's "UIReturnKeyDone". The title displayed in
337    /// the action button is "Done".
338    Done,
339
340    /// Logical meaning: The user has entered some text that represents a
341    /// destination, e.g., a restaurant name. The "go" button is intended to take
342    /// the user to a part of the app that corresponds to this destination.
343    ///
344    /// Android: Corresponds to Android's "IME_ACTION_GO". The OS displays a
345    /// button that represents taking "the user to the target of the text they
346    /// typed", e.g., a right-facing arrow button.
347    ///
348    /// iOS: Corresponds to iOS's "UIReturnKeyGo". The title displayed in the
349    /// action button is "Go".
350    Go,
351
352    /// Logical meaning: Execute a search query.
353    ///
354    /// Android: Corresponds to Android's "IME_ACTION_SEARCH". The OS displays a
355    /// button that represents a search, e.g., a magnifying glass button.
356    ///
357    /// iOS: Corresponds to iOS's "UIReturnKeySearch". The title displayed in
358    /// the action button is "Search".
359    Search,
360
361    /// Logical meaning: Sends something that the user has composed, e.g., an
362    /// email or a text message.
363    ///
364    /// Android: Corresponds to Android's "IME_ACTION_SEND". The OS displays a
365    /// button that represents sending something, e.g., a paper plane button.
366    ///
367    /// iOS: Corresponds to iOS's "UIReturnKeySend". The title displayed in the
368    /// action button is "Send".
369    Send,
370
371    /// Logical meaning: The user is done with the current input source and wants
372    /// to move to the next one.
373    ///
374    /// Moves the focus to the next focusable item in the same `FocusScope`.
375    ///
376    /// Android: Corresponds to Android's "IME_ACTION_NEXT". The OS displays a
377    /// button that represents moving forward, e.g., a right-facing arrow button.
378    ///
379    /// iOS: Corresponds to iOS's "UIReturnKeyNext". The title displayed in the
380    /// action button is "Next".
381    Next,
382
383    /// Logical meaning: The user wishes to return to the previous input source
384    /// in the group, e.g., a form with multiple `TextField`s.
385    ///
386    /// Moves the focus to the previous focusable item in the same `FocusScope`.
387    ///
388    /// Android: Corresponds to Android's "IME_ACTION_PREVIOUS". The OS displays a
389    /// button that represents moving backward, e.g., a left-facing arrow button.
390    ///
391    /// iOS: iOS does not have a keyboard return type of "previous." It is
392    /// inappropriate to choose this [`TextInputAction`] when running on iOS.
393    Previous,
394
395    /// Logical meaning: In iOS apps, it is common for a "Back" button and
396    /// "Continue" button to appear at the top of the screen. However, when the
397    /// keyboard is open, these buttons are often hidden off-screen. Therefore,
398    /// the purpose of the "Continue" return key on iOS is to make the "Continue"
399    /// button available when the user is entering text.
400    ///
401    /// Historical context aside, [`ContinueAction`](TextInputAction::ContinueAction) can be used any
402    /// time that the term "Continue" seems most appropriate for the given action.
403    ///
404    /// Android: Android does not have an IME input type of "continue." It is
405    /// inappropriate to choose this [`TextInputAction`] when running on Android.
406    ///
407    /// iOS: Corresponds to iOS's "UIReturnKeyContinue". The title displayed in the
408    /// action button is "Continue". This action is only available on iOS 9.0+.
409    ///
410    /// The reason that this value has "Action" post-fixed to it is because
411    /// "continue" is a reserved word in Dart, as well as many other languages.
412    ContinueAction,
413
414    /// Logical meaning: The user wants to join something, e.g., a wireless
415    /// network.
416    ///
417    /// Android: Android does not have an IME input type of "join." It is
418    /// inappropriate to choose this [`TextInputAction`] when running on Android.
419    ///
420    /// iOS: Corresponds to iOS's "UIReturnKeyJoin". The title displayed in the
421    /// action button is "Join".
422    Join,
423
424    /// Logical meaning: The user wants routing options, e.g., driving directions.
425    ///
426    /// Android: Android does not have an IME input type of "route." It is
427    /// inappropriate to choose this [`TextInputAction`] when running on Android.
428    ///
429    /// iOS: Corresponds to iOS's "UIReturnKeyRoute". The title displayed in the
430    /// action button is "Route".
431    Route,
432
433    /// Logical meaning: Initiate a call to emergency services.
434    ///
435    /// Android: Android does not have an IME input type of "emergencyCall." It is
436    /// inappropriate to choose this [`TextInputAction`] when running on Android.
437    ///
438    /// iOS: Corresponds to iOS's "UIReturnKeyEmergencyCall". The title displayed
439    /// in the action button is "Emergency Call".
440    EmergencyCall,
441
442    /// Logical meaning: Insert a newline character in the focused text input,
443    /// e.g., `TextField`.
444    ///
445    /// Android: Corresponds to Android's "IME_ACTION_NONE". The OS displays a
446    /// button that represents a new line, e.g., a carriage return button.
447    ///
448    /// iOS: Corresponds to iOS's "UIReturnKeyDefault". The title displayed in
449    /// the action button is "return".
450    ///
451    /// The term [`Newline`](TextInputAction::Newline) exists in Flutter but not in Android
452    /// or iOS. The reason for introducing this term is so that developers can
453    /// achieve the common result of inserting new lines without needing to
454    /// understand the various IME actions on Android and return keys on iOS.
455    /// Thus, [`Newline`](TextInputAction::Newline) is a convenience term that alleviates the
456    /// need to understand the underlying platforms to achieve this common behavior.
457    Newline,
458}
459
460/// Configures how the platform keyboard will select an uppercase or
461/// lowercase keyboard.
462///
463/// Only supports text keyboards, other keyboard types will ignore this
464/// configuration. Capitalization is locale-aware.
465#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
466pub enum TextCapitalization {
467    /// Defaults to an uppercase keyboard for the first letter of each word.
468    ///
469    /// Corresponds to `InputType.TYPE_TEXT_FLAG_CAP_WORDS` on Android, and
470    /// `UITextAutocapitalizationTypeWords` on iOS.
471    Words,
472
473    /// Defaults to an uppercase keyboard for the first letter of each sentence.
474    ///
475    /// Corresponds to `InputType.TYPE_TEXT_FLAG_CAP_SENTENCES` on Android, and
476    /// `UITextAutocapitalizationTypeSentences` on iOS.
477    Sentences,
478
479    /// Defaults to an uppercase keyboard for each character.
480    ///
481    /// Corresponds to `InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS` on Android, and
482    /// `UITextAutocapitalizationTypeAllCharacters` on iOS.
483    Characters,
484
485    /// Defaults to a lowercase keyboard.
486    None,
487}
488
489/// A collection of autofill related information that represents an `AutofillClient`.
490///
491/// Typically used in [`TextInputConfiguration::autofill_configuration`].
492///
493/// Lives here with [`TextInputConfiguration`] because [`crate::View`] names that
494/// type. Flutter's file is `services/autofill.dart`.
495#[derive(Clone, Debug, PartialEq, Eq, Hash)]
496pub struct AutofillConfiguration {
497    /// Whether autofill should be enabled for the `AutofillClient`.
498    ///
499    /// To retrieve a disabled [`AutofillConfiguration`], use [`DISABLED`](Self::DISABLED).
500    pub enabled: bool,
501    /// A string that uniquely identifies the current `AutofillClient`.
502    ///
503    /// The identifier needs to be unique within the `AutofillScope` for the
504    /// `AutofillClient` to receive the correct autofill value.
505    pub unique_identifier: String,
506    /// A list of strings that helps the autofill service identify the type of the
507    /// `AutofillClient`.
508    ///
509    /// See `AutofillHints` (`services/autofill.dart`).
510    pub autofill_hints: Vec<String>,
511    /// The current [`TextEditingValue`] of the `AutofillClient`.
512    pub current_editing_value: TextEditingValue,
513    /// The optional hint text placed on the view that typically suggests what
514    /// sort of input the field accepts, for example "enter your password here".
515    ///
516    /// If the developer does not specify any [`autofill_hints`](Self::autofill_hints), the
517    /// [`hint_text`](Self::hint_text) can be a useful indication to the platform autofill service.
518    pub hint_text: Option<String>,
519}
520
521impl AutofillConfiguration {
522    /// An [`AutofillConfiguration`] that indicates the `AutofillClient` does not
523    /// wish to be autofilled.
524    pub const DISABLED: AutofillConfiguration = AutofillConfiguration {
525        enabled: false,
526        unique_identifier: String::new(),
527        autofill_hints: Vec::new(),
528        current_editing_value: TextEditingValue::EMPTY,
529        hint_text: None,
530    };
531
532    /// Creates autofill related configuration information that can be sent to the
533    /// platform.
534    pub fn new(
535        unique_identifier: impl Into<String>,
536        autofill_hints: Vec<String>,
537        current_editing_value: TextEditingValue,
538    ) -> AutofillConfiguration {
539        AutofillConfiguration {
540            enabled: true,
541            unique_identifier: unique_identifier.into(),
542            autofill_hints,
543            current_editing_value,
544            hint_text: None,
545        }
546    }
547
548    /// Dart `AutofillConfiguration(hintText:)`.
549    pub fn hint_text(mut self, hint_text: impl Into<String>) -> AutofillConfiguration {
550        self.hint_text = Some(hint_text.into());
551        self
552    }
553}
554
555impl fmt::Display for AutofillConfiguration {
556    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
557        write!(
558            f,
559            "AutofillConfiguration(enabled: {}, uniqueIdentifier: {}, autofillHints: {:?}, currentEditingValue: {}",
560            self.enabled, self.unique_identifier, self.autofill_hints, self.current_editing_value
561        )?;
562        if let Some(hint_text) = &self.hint_text {
563            write!(f, ", hintText: {hint_text}")?;
564        }
565        write!(f, ")")
566    }
567}
568
569/// Controls the visual appearance of the text input control.
570///
571/// Many [`TextInputAction`]s are common between Android and iOS. However, if an
572/// [`input_action`](Self::input_action) is provided that is not supported by the current
573/// platform in debug mode, an error will be thrown when the corresponding
574/// text input is attached. For example, providing iOS's "emergencyCall"
575/// action when running on an Android device will result in an error when in
576/// debug mode. In release mode, incompatible [`TextInputAction`]s are replaced
577/// either with "unspecified" on Android, or "default" on iOS. Appropriate
578/// [`input_action`](Self::input_action)s can be chosen by checking the current platform and then
579/// selecting the appropriate action.
580///
581/// See also:
582///
583///  * `TextInput.attach`
584///  * [`TextInputAction`]
585#[derive(Clone, Debug, PartialEq)]
586pub struct TextInputConfiguration {
587    /// The ID of the view that the text input belongs to.
588    pub view_id: Option<i32>,
589    /// The type of information for which to optimize the text input control.
590    pub input_type: TextInputType,
591    /// Whether the text field can be edited or not.
592    ///
593    /// Defaults to false.
594    pub read_only: bool,
595    /// Whether to hide the text being edited (e.g., for passwords).
596    ///
597    /// Defaults to false.
598    pub obscure_text: bool,
599    /// Whether to enable autocorrection.
600    ///
601    /// Defaults to true.
602    pub autocorrect: bool,
603    /// Whether to allow the platform to automatically format dashes.
604    pub smart_dashes_type: SmartDashesType,
605    /// Whether to allow the platform to automatically format quotes.
606    pub smart_quotes_type: SmartQuotesType,
607    /// Whether to show input suggestions as the user types.
608    ///
609    /// Defaults to true.
610    pub enable_suggestions: bool,
611    /// Whether a user can change its selection.
612    ///
613    /// Defaults to true.
614    pub enable_interactive_selection: bool,
615    /// What text to display in the text input control's action button.
616    pub action_label: Option<String>,
617    /// What kind of action to request for the action button on the IME.
618    pub input_action: TextInputAction,
619    /// Specifies how platforms may automatically capitalize text entered by the
620    /// user.
621    ///
622    /// Defaults to [`TextCapitalization::None`].
623    pub text_capitalization: TextCapitalization,
624    /// The appearance of the keyboard.
625    ///
626    /// This setting is only honored on iOS devices.
627    ///
628    /// Defaults to [`Brightness::Light`].
629    pub keyboard_appearance: Brightness,
630    /// The configuration to use for autofill.
631    pub autofill_configuration: AutofillConfiguration,
632    /// Whether to enable that the IME update personalized data such as typing
633    /// history and user dictionary data.
634    ///
635    /// Defaults to true.
636    pub enable_ime_personalized_learning: bool,
637    /// MIME types the field accepts for content insertion.
638    pub allowed_mime_types: Vec<String>,
639    /// Whether to enable that the engine sends text input updates to the
640    /// framework as `TextEditingDelta`s or as one [`TextEditingValue`].
641    ///
642    /// Defaults to false.
643    pub enable_delta_model: bool,
644    /// List of the languages that the user is expected to use.
645    ///
646    /// Pass an empty list to express the intention that a specific hint should not be set.
647    pub hint_locales: Vec<Locale>,
648    /// Whether to enable inline predictive text.
649    ///
650    /// This feature is specific to iOS 17 and later. `None` leaves the platform default.
651    pub enable_inline_prediction: Option<bool>,
652}
653
654impl TextInputConfiguration {
655    /// Creates configuration information for a text input control.
656    pub fn new() -> TextInputConfiguration {
657        TextInputConfiguration {
658            view_id: None,
659            input_type: TextInputType::TEXT,
660            read_only: false,
661            obscure_text: false,
662            autocorrect: true,
663            smart_dashes_type: SmartDashesType::Enabled,
664            smart_quotes_type: SmartQuotesType::Enabled,
665            enable_suggestions: true,
666            enable_interactive_selection: true,
667            action_label: None,
668            input_action: TextInputAction::Done,
669            text_capitalization: TextCapitalization::None,
670            keyboard_appearance: Brightness::Light,
671            autofill_configuration: AutofillConfiguration::DISABLED,
672            enable_ime_personalized_learning: true,
673            allowed_mime_types: Vec::new(),
674            enable_delta_model: false,
675            hint_locales: Vec::new(),
676            enable_inline_prediction: None,
677        }
678    }
679
680    /// Dart `TextInputConfiguration(viewId:)`.
681    pub fn view_id(mut self, view_id: i32) -> TextInputConfiguration {
682        self.view_id = Some(view_id);
683        self
684    }
685
686    /// Dart `TextInputConfiguration(inputType:)`.
687    pub fn input_type(mut self, input_type: TextInputType) -> TextInputConfiguration {
688        self.input_type = input_type;
689        self
690    }
691
692    /// Dart `TextInputConfiguration(readOnly:)`.
693    pub fn read_only(mut self, read_only: bool) -> TextInputConfiguration {
694        self.read_only = read_only;
695        self
696    }
697
698    /// Dart `TextInputConfiguration(obscureText:)`.
699    pub fn obscure_text(mut self, obscure_text: bool) -> TextInputConfiguration {
700        self.obscure_text = obscure_text;
701        self.smart_dashes_type = if obscure_text {
702            SmartDashesType::Disabled
703        } else {
704            SmartDashesType::Enabled
705        };
706        self.smart_quotes_type = if obscure_text {
707            SmartQuotesType::Disabled
708        } else {
709            SmartQuotesType::Enabled
710        };
711        self
712    }
713
714    /// Dart `TextInputConfiguration(autocorrect:)`.
715    pub fn autocorrect(mut self, autocorrect: bool) -> TextInputConfiguration {
716        self.autocorrect = autocorrect;
717        self
718    }
719
720    /// Dart `TextInputConfiguration(smartDashesType:)`.
721    pub fn smart_dashes_type(
722        mut self,
723        smart_dashes_type: SmartDashesType,
724    ) -> TextInputConfiguration {
725        self.smart_dashes_type = smart_dashes_type;
726        self
727    }
728
729    /// Dart `TextInputConfiguration(smartQuotesType:)`.
730    pub fn smart_quotes_type(
731        mut self,
732        smart_quotes_type: SmartQuotesType,
733    ) -> TextInputConfiguration {
734        self.smart_quotes_type = smart_quotes_type;
735        self
736    }
737
738    /// Dart `TextInputConfiguration(enableSuggestions:)`.
739    pub fn enable_suggestions(mut self, enable_suggestions: bool) -> TextInputConfiguration {
740        self.enable_suggestions = enable_suggestions;
741        self
742    }
743
744    /// Dart `TextInputConfiguration(enableInteractiveSelection:)`.
745    pub fn enable_interactive_selection(
746        mut self,
747        enable_interactive_selection: bool,
748    ) -> TextInputConfiguration {
749        self.enable_interactive_selection = enable_interactive_selection;
750        self
751    }
752
753    /// Dart `TextInputConfiguration(actionLabel:)`.
754    pub fn action_label(mut self, action_label: impl Into<String>) -> TextInputConfiguration {
755        self.action_label = Some(action_label.into());
756        self
757    }
758
759    /// Dart `TextInputConfiguration(inputAction:)`.
760    pub fn input_action(mut self, input_action: TextInputAction) -> TextInputConfiguration {
761        self.input_action = input_action;
762        self
763    }
764
765    /// Dart `TextInputConfiguration(keyboardAppearance:)`.
766    pub fn keyboard_appearance(
767        mut self,
768        keyboard_appearance: Brightness,
769    ) -> TextInputConfiguration {
770        self.keyboard_appearance = keyboard_appearance;
771        self
772    }
773
774    /// Dart `TextInputConfiguration(textCapitalization:)`.
775    pub fn text_capitalization(
776        mut self,
777        text_capitalization: TextCapitalization,
778    ) -> TextInputConfiguration {
779        self.text_capitalization = text_capitalization;
780        self
781    }
782
783    /// Dart `TextInputConfiguration(autofillConfiguration:)`.
784    pub fn autofill_configuration(
785        mut self,
786        autofill_configuration: AutofillConfiguration,
787    ) -> TextInputConfiguration {
788        self.autofill_configuration = autofill_configuration;
789        self
790    }
791
792    /// Dart `TextInputConfiguration(enableIMEPersonalizedLearning:)`.
793    pub fn enable_ime_personalized_learning(
794        mut self,
795        enable_ime_personalized_learning: bool,
796    ) -> TextInputConfiguration {
797        self.enable_ime_personalized_learning = enable_ime_personalized_learning;
798        self
799    }
800
801    /// Dart `TextInputConfiguration(allowedMimeTypes:)`.
802    pub fn allowed_mime_types(mut self, allowed_mime_types: Vec<String>) -> TextInputConfiguration {
803        self.allowed_mime_types = allowed_mime_types;
804        self
805    }
806
807    /// Dart `TextInputConfiguration(enableDeltaModel:)`.
808    pub fn enable_delta_model(mut self, enable_delta_model: bool) -> TextInputConfiguration {
809        self.enable_delta_model = enable_delta_model;
810        self
811    }
812
813    /// Dart `TextInputConfiguration(hintLocales:)`.
814    pub fn hint_locales(mut self, hint_locales: Vec<Locale>) -> TextInputConfiguration {
815        self.hint_locales = hint_locales;
816        self
817    }
818
819    /// Dart `TextInputConfiguration(enableInlinePrediction:)`.
820    pub fn enable_inline_prediction(
821        mut self,
822        enable_inline_prediction: bool,
823    ) -> TextInputConfiguration {
824        self.enable_inline_prediction = Some(enable_inline_prediction);
825        self
826    }
827
828    /// Creates a copy of this [`TextInputConfiguration`] with the given fields
829    /// replaced with new values.
830    pub fn copy_with(&self) -> TextInputConfiguration {
831        self.clone()
832    }
833}
834
835impl Default for TextInputConfiguration {
836    fn default() -> TextInputConfiguration {
837        TextInputConfiguration::new()
838    }
839}
840
841impl fmt::Display for TextInputConfiguration {
842    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
843        let mut parts = vec![
844            format!("inputType: {}", self.input_type),
845            format!("readOnly: {}", self.read_only),
846            format!("obscureText: {}", self.obscure_text),
847            format!("autocorrect: {}", self.autocorrect),
848            format!("smartDashesType: {:?}", self.smart_dashes_type),
849            format!("smartQuotesType: {:?}", self.smart_quotes_type),
850            format!("enableSuggestions: {}", self.enable_suggestions),
851            format!(
852                "enableInteractiveSelection: {}",
853                self.enable_interactive_selection
854            ),
855            format!("inputAction: {:?}", self.input_action),
856            format!("keyboardAppearance: {:?}", self.keyboard_appearance),
857            format!("textCapitalization: {:?}", self.text_capitalization),
858            format!("autofillConfiguration: {}", self.autofill_configuration),
859            format!(
860                "enableIMEPersonalizedLearning: {}",
861                self.enable_ime_personalized_learning
862            ),
863            format!("allowedMimeTypes: {:?}", self.allowed_mime_types),
864            format!("enableDeltaModel: {}", self.enable_delta_model),
865        ];
866        if let Some(view_id) = self.view_id {
867            parts.insert(0, format!("viewId: {view_id}"));
868        }
869        if let Some(action_label) = &self.action_label {
870            parts.insert(
871                parts
872                    .iter()
873                    .position(|p| p.starts_with("inputAction:"))
874                    .unwrap_or(parts.len()),
875                format!("actionLabel: {action_label}"),
876            );
877        }
878        if !self.hint_locales.is_empty() {
879            parts.push(format!("hintLocales: {:?}", self.hint_locales));
880        }
881        if let Some(enable_inline_prediction) = self.enable_inline_prediction {
882            parts.push(format!(
883                "enableInlinePrediction: {enable_inline_prediction}"
884            ));
885        }
886        write!(f, "TextInputConfiguration({})", parts.join(", "))
887    }
888}
889
890/// The state of a "floating cursor" drag on an iOS soft keyboard.
891///
892/// The "floating cursor" cursor-positioning mode is an iOS feature used to
893/// precisely position the caret in some editable text using certain touch
894/// gestures. As an example, when the user long-presses the spacebar on the iOS
895/// virtual keyboard, iOS enters floating cursor mode where the whole keyboard
896/// becomes a trackpad. In this mode, there are two visible cursors. One, the
897/// floating cursor, hovers over the text, following the user's horizontal
898/// movements exactly and snapping to lines vertically. The other, the
899/// placeholder cursor, is a "shadow" that also snaps to the actual location
900/// where the cursor will go horizontally when the user releases the trackpad.
901///
902/// The floating cursor renders over the text field, while the placeholder
903/// cursor is a faint shadow of the cursor rendered in the text field in the
904/// location between characters where the cursor will drop into when released.
905/// The placeholder cursor is a faint vertical bar, while the floating cursor
906/// has the same appearance as a normal cursor (a blue vertical bar).
907///
908/// This feature works out-of-the-box with Flutter. Support is built into
909/// `EditableText`.
910///
911/// See also:
912///
913///  * `EditableText.backgroundCursorColor`, which configures the color of the
914///    placeholder cursor while the floating cursor is being dragged.
915#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
916pub enum FloatingCursorDragState {
917    /// A user has just activated a floating cursor by long pressing on the
918    /// spacebar.
919    Start,
920
921    /// A user is dragging a floating cursor.
922    Update,
923
924    /// A user has lifted their finger off the screen after using a floating
925    /// cursor.
926    End,
927}
928
929/// The current state and position of the floating cursor.
930///
931/// See also:
932///
933///  * [`FloatingCursorDragState`], which explains the floating cursor feature in
934///    detail.
935#[derive(Clone, Copy, Debug, PartialEq)]
936pub struct RawFloatingCursorPoint {
937    /// The raw position of the floating cursor as determined by the iOS sdk.
938    pub offset: Option<Offset>,
939    /// Represents the starting location when initiating a floating cursor via long press.
940    /// This is a tuple where the first item is the local offset and the second item is the new caret position.
941    /// This is only non-null when a floating cursor is started.
942    pub start_location: Option<(Offset, TextPosition)>,
943    /// The state of the floating cursor.
944    pub state: FloatingCursorDragState,
945}
946
947impl RawFloatingCursorPoint {
948    /// Creates information for setting the position and state of a floating
949    /// cursor.
950    ///
951    /// [`state`](Self::state) must not be null and [`offset`](Self::offset) must not be null if the state is
952    /// [`FloatingCursorDragState::Update`].
953    pub fn new(state: FloatingCursorDragState) -> RawFloatingCursorPoint {
954        let point = RawFloatingCursorPoint {
955            offset: None,
956            start_location: None,
957            state,
958        };
959        if state == FloatingCursorDragState::Update {
960            point
961        } else {
962            point.assemble()
963        }
964    }
965
966    /// Dart `RawFloatingCursorPoint(offset:)`.
967    pub fn offset(mut self, offset: Offset) -> RawFloatingCursorPoint {
968        self.offset = Some(offset);
969        self.assemble()
970    }
971
972    /// Dart `RawFloatingCursorPoint(startLocation:)`.
973    pub fn start_location(
974        mut self,
975        start_location: (Offset, TextPosition),
976    ) -> RawFloatingCursorPoint {
977        self.start_location = Some(start_location);
978        self
979    }
980
981    fn assemble(self) -> RawFloatingCursorPoint {
982        debug_assert!(
983            self.state != FloatingCursorDragState::Update || self.offset.is_some(),
984            "offset must not be null if the state is Update"
985        );
986        self
987    }
988}
989
990/// The current text, selection, and composing state for editing a run of text.
991#[derive(Clone, Debug, PartialEq, Eq, Hash)]
992pub struct TextEditingValue {
993    /// The current text being edited.
994    pub text: String,
995    /// The range of text that is currently selected.
996    ///
997    /// When [`selection`](Self::selection) is a [`TextSelection`] that has the same non-negative
998    /// `baseOffset` and `extentOffset`, the [`selection`](Self::selection) property represents the
999    /// caret position.
1000    ///
1001    /// If the current [`selection`](Self::selection) has a negative `baseOffset` or `extentOffset`,
1002    /// then the text currently does not have a selection or a caret location, and
1003    /// most text editing operations that rely on the current selection (for
1004    /// instance, insert a character at the caret location) will do nothing.
1005    pub selection: TextSelection,
1006    /// The range of text that is still being composed.
1007    ///
1008    /// Composing regions are created by input methods (IMEs) to indicate the text
1009    /// within a certain range is provisional. For instance, the Android Gboard
1010    /// app's English keyboard puts the current word under the caret into a
1011    /// composing region to indicate the word is subject to autocorrect or
1012    /// prediction changes.
1013    ///
1014    /// Composing regions can also be used for performing multistage input, which
1015    /// is typically used by IMEs designed for phonetic keyboard to enter
1016    /// ideographic symbols. As an example, many CJK keyboards require the user to
1017    /// enter a Latin alphabet sequence and then convert it to CJK characters. On
1018    /// iOS, the default software keyboards do not have a dedicated view to show
1019    /// the unfinished Latin sequence, so it's displayed directly in the text
1020    /// field, inside of a composing region.
1021    ///
1022    /// On iOS 17 and later, the composing region can also be used
1023    /// to display inline text predictions. The user can accept the
1024    /// predicted text by tapping the Space bar.
1025    ///
1026    /// The composing region should typically only be changed by the IME, or the
1027    /// user via interacting with the IME.
1028    ///
1029    /// If the range represented by this property is [`TextRange::EMPTY`], then the
1030    /// text is not currently being composed.
1031    pub composing: TextRange,
1032}
1033
1034impl TextEditingValue {
1035    /// A value that corresponds to the empty string with no selection and no composing range.
1036    pub const EMPTY: TextEditingValue = TextEditingValue {
1037        text: String::new(),
1038        selection: TextSelection::collapsed(-1, TextAffinity::Downstream),
1039        composing: TextRange::EMPTY,
1040    };
1041
1042    /// Creates information for editing a run of text.
1043    ///
1044    /// The selection and composing range must be within the text. This is not
1045    /// checked during construction, and must be guaranteed by the caller.
1046    ///
1047    /// The default value of [`selection`](Self::selection) is `TextSelection::collapsed(-1, …)`.
1048    /// This indicates that there is no selection at all.
1049    pub fn new() -> TextEditingValue {
1050        TextEditingValue::EMPTY
1051    }
1052
1053    /// Dart `TextEditingValue(text:)`.
1054    pub fn text(mut self, text: impl Into<String>) -> TextEditingValue {
1055        self.text = text.into();
1056        self
1057    }
1058
1059    /// Dart `TextEditingValue(selection:)`.
1060    pub fn selection(mut self, selection: TextSelection) -> TextEditingValue {
1061        self.selection = selection;
1062        self
1063    }
1064
1065    /// Dart `TextEditingValue(composing:)`.
1066    pub fn composing(mut self, composing: TextRange) -> TextEditingValue {
1067        self.composing = composing;
1068        self
1069    }
1070
1071    /// Creates a copy of this value but with the given fields replaced with the new values.
1072    pub fn copy_with(&self) -> TextEditingValue {
1073        self.clone()
1074    }
1075
1076    /// Whether the [`composing`](Self::composing) range is a valid range within [`text`](Self::text).
1077    ///
1078    /// Returns true if and only if the [`composing`](Self::composing) range is normalized, its start
1079    /// is greater than or equal to 0, and its end is less than or equal to
1080    /// [`text`](Self::text)'s length.
1081    ///
1082    /// If this property is false while the [`composing`](Self::composing) range's `isValid` is true,
1083    /// it usually indicates the current [`composing`](Self::composing) range is invalid because of a
1084    /// programming error.
1085    pub fn is_composing_range_valid(&self) -> bool {
1086        self.composing.is_valid()
1087            && self.composing.is_normalized()
1088            && self.composing.end <= utf16_len(&self.text)
1089    }
1090
1091    /// Returns a new [`TextEditingValue`], which is this [`TextEditingValue`] with
1092    /// its [`text`](Self::text) partially replaced by the `replacement_string`.
1093    ///
1094    /// The `replacement_range` parameter specifies the range of the
1095    /// [`text`](Self::text) that needs to be replaced.
1096    ///
1097    /// The `replacement_string` parameter specifies the string to replace the
1098    /// given range of text with.
1099    ///
1100    /// This method also adjusts the selection range and the composing range of the
1101    /// resulting [`TextEditingValue`], such that they point to the same substrings
1102    /// as the corresponding ranges in the original [`TextEditingValue`]. For
1103    /// example, if the original [`TextEditingValue`] is "Hello world" with the word
1104    /// "world" selected, replacing "Hello" with a different string using this
1105    /// method will not change the selected word.
1106    ///
1107    /// This method does nothing if the given `replacement_range` is not
1108    /// [`TextRange::is_valid`].
1109    pub fn replaced(
1110        &self,
1111        replacement_range: impl Into<TextRange>,
1112        replacement_string: &str,
1113    ) -> TextEditingValue {
1114        let replacement_range = replacement_range.into();
1115        if !replacement_range.is_valid() {
1116            return self.clone();
1117        }
1118        debug_assert!(replacement_range.is_normalized());
1119        let new_text = format!(
1120            "{}{}{}",
1121            replacement_range.text_before(&self.text),
1122            replacement_string,
1123            replacement_range.text_after(&self.text),
1124        );
1125        if replacement_range.end - replacement_range.start == utf16_len(replacement_string) {
1126            return self.copy_with().text(new_text);
1127        }
1128
1129        let replacement_len = utf16_len(replacement_string);
1130        let adjust_index = |original_index: i32| {
1131            // The length added by adding the replacementString.
1132            let replaced_length = if original_index <= replacement_range.start
1133                && original_index < replacement_range.end
1134            {
1135                0
1136            } else {
1137                replacement_len
1138            };
1139            // The length removed by removing the replacementRange.
1140            let removed_length = original_index
1141                .clamp(replacement_range.start, replacement_range.end)
1142                - replacement_range.start;
1143            original_index + replaced_length - removed_length
1144        };
1145
1146        let adjusted_selection = TextSelection::new(
1147            adjust_index(self.selection.base_offset),
1148            adjust_index(self.selection.extent_offset),
1149        );
1150        let adjusted_composing = TextRange::new(
1151            adjust_index(self.composing.start),
1152            adjust_index(self.composing.end),
1153        );
1154        debug_assert!(text_range_is_valid(adjusted_selection.range(), &new_text));
1155        debug_assert!(text_range_is_valid(adjusted_composing, &new_text));
1156        TextEditingValue::new()
1157            .text(new_text)
1158            .selection(adjusted_selection)
1159            .composing(adjusted_composing)
1160    }
1161}
1162
1163impl Default for TextEditingValue {
1164    fn default() -> TextEditingValue {
1165        TextEditingValue::new()
1166    }
1167}
1168
1169impl fmt::Display for TextEditingValue {
1170    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1171        write!(
1172            f,
1173            "TextEditingValue(text: \u{2524}{}\u{251C}, selection: {}, composing: {:?})",
1174            self.text, self.selection, self.composing
1175        )
1176    }
1177}
1178
1179/// Dart `String.length`: UTF-16 code units.
1180fn utf16_len(text: &str) -> i32 {
1181    text.encode_utf16().count() as i32
1182}
1183
1184/// Verify that the given range is within the text.
1185///
1186/// The verification can't be perform during the constructor of
1187/// [`TextEditingValue`], which are `const` and are allowed to retrieve
1188/// properties of [`TextRange`]s. [`TextEditingValue`] should perform this
1189/// wherever it is building other values (such as toJson) or is built in a
1190/// non-const way (such as fromJson).
1191fn text_range_is_valid(range: TextRange, text: &str) -> bool {
1192    if range.start == -1 && range.end == -1 {
1193        return true;
1194    }
1195    let len = utf16_len(text);
1196    debug_assert!(
1197        range.start >= 0 && range.start <= len,
1198        "Range start {} is out of text of length {len}",
1199        range.start
1200    );
1201    debug_assert!(
1202        range.end >= 0 && range.end <= len,
1203        "Range end {} is out of text of length {len}",
1204        range.end
1205    );
1206    true
1207}
1208
1209#[cfg(test)]
1210mod tests {
1211    use super::*;
1212
1213    const TEST_TEXT: &str = "From a false proposition, anything follows.";
1214
1215    #[test]
1216    fn replaced_deletes_a_selection() {
1217        let selection = TextSelection::new(5, 13);
1218        assert_eq!(
1219            TextEditingValue::new()
1220                .text(TEST_TEXT)
1221                .selection(selection)
1222                .replaced(selection, ""),
1223            TextEditingValue::new()
1224                .text("From proposition, anything follows.")
1225                .selection(TextSelection::collapsed(5, TextAffinity::Downstream))
1226        );
1227    }
1228
1229    #[test]
1230    fn replaced_deletes_a_reversed_selection() {
1231        let selection = TextSelection::new(13, 5);
1232        assert_eq!(
1233            TextEditingValue::new()
1234                .text(TEST_TEXT)
1235                .selection(selection)
1236                .replaced(selection, ""),
1237            TextEditingValue::new()
1238                .text("From proposition, anything follows.")
1239                .selection(TextSelection::collapsed(5, TextAffinity::Downstream))
1240        );
1241    }
1242
1243    #[test]
1244    fn replaced_inserts_at_the_caret() {
1245        let selection = TextSelection::collapsed(5, TextAffinity::Downstream);
1246        assert_eq!(
1247            TextEditingValue::new()
1248                .text(TEST_TEXT)
1249                .selection(selection)
1250                .replaced(selection, "AA"),
1251            TextEditingValue::new()
1252                .text("From AAa false proposition, anything follows.")
1253                .selection(TextSelection::collapsed(7, TextAffinity::Downstream))
1254        );
1255    }
1256
1257    #[test]
1258    fn replaced_before_selection_shifts_the_range() {
1259        let selection = TextSelection::new(13, 5);
1260        assert_eq!(
1261            TextEditingValue::new()
1262                .text(TEST_TEXT)
1263                .selection(selection)
1264                .replaced(TextRange::new(4, 5), "AA"),
1265            TextEditingValue::new()
1266                .text("FromAAa false proposition, anything follows.")
1267                .selection(TextSelection::new(14, 6))
1268        );
1269    }
1270
1271    #[test]
1272    fn replaced_after_selection_leaves_the_range() {
1273        let selection = TextSelection::new(13, 5);
1274        assert_eq!(
1275            TextEditingValue::new()
1276                .text(TEST_TEXT)
1277                .selection(selection)
1278                .replaced(TextRange::new(13, 14), "AA"),
1279            TextEditingValue::new()
1280                .text("From a false AAroposition, anything follows.")
1281                .selection(selection)
1282        );
1283    }
1284
1285    #[test]
1286    fn replaced_inside_selection_start_boundary() {
1287        let selection = TextSelection::new(13, 5);
1288        assert_eq!(
1289            TextEditingValue::new()
1290                .text(TEST_TEXT)
1291                .selection(selection)
1292                .replaced(TextRange::new(5, 6), "AA"),
1293            TextEditingValue::new()
1294                .text("From AA false proposition, anything follows.")
1295                .selection(TextSelection::new(14, 5))
1296        );
1297    }
1298
1299    #[test]
1300    fn replaced_inside_selection_end_boundary() {
1301        let selection = TextSelection::new(13, 5);
1302        assert_eq!(
1303            TextEditingValue::new()
1304                .text(TEST_TEXT)
1305                .selection(selection)
1306                .replaced(TextRange::new(12, 13), "AA"),
1307            TextEditingValue::new()
1308                .text("From a falseAAproposition, anything follows.")
1309                .selection(TextSelection::new(14, 5))
1310        );
1311    }
1312
1313    #[test]
1314    fn replaced_deletes_after_selection() {
1315        let selection = TextSelection::new(13, 5);
1316        assert_eq!(
1317            TextEditingValue::new()
1318                .text(TEST_TEXT)
1319                .selection(selection)
1320                .replaced(TextRange::new(13, 14), ""),
1321            TextEditingValue::new()
1322                .text("From a false roposition, anything follows.")
1323                .selection(selection)
1324        );
1325    }
1326
1327    #[test]
1328    fn replaced_deletes_inside_selection_start_boundary() {
1329        let selection = TextSelection::new(13, 5);
1330        assert_eq!(
1331            TextEditingValue::new()
1332                .text(TEST_TEXT)
1333                .selection(selection)
1334                .replaced(TextRange::new(5, 6), ""),
1335            TextEditingValue::new()
1336                .text("From  false proposition, anything follows.")
1337                .selection(TextSelection::new(12, 5))
1338        );
1339    }
1340
1341    #[test]
1342    fn replaced_deletes_inside_selection_end_boundary() {
1343        let selection = TextSelection::new(13, 5);
1344        assert_eq!(
1345            TextEditingValue::new()
1346                .text(TEST_TEXT)
1347                .selection(selection)
1348                .replaced(TextRange::new(12, 13), ""),
1349            TextEditingValue::new()
1350                .text("From a falseproposition, anything follows.")
1351                .selection(TextSelection::new(12, 5))
1352        );
1353    }
1354
1355    #[test]
1356    fn empty_prints_like_dart() {
1357        assert_eq!(
1358            TextEditingValue::EMPTY.to_string(),
1359            format!(
1360                "TextEditingValue(text: \u{2524}\u{251C}, selection: {}, composing: {:?})",
1361                TextSelection::collapsed(-1, TextAffinity::Downstream),
1362                TextRange::EMPTY
1363            )
1364        );
1365    }
1366
1367    #[test]
1368    fn number_with_options_defaults_match_dart() {
1369        assert_eq!(TextInputType::NUMBER.signed, Some(false));
1370        assert_eq!(
1371            TextInputType::number_with_options()
1372                .signed(true)
1373                .decimal(true),
1374            TextInputType {
1375                index: 2,
1376                signed: Some(true),
1377                decimal: Some(true),
1378                password: Some(false),
1379            }
1380        );
1381    }
1382
1383    #[test]
1384    fn obscure_text_disables_smart_punctuation_unless_overridden() {
1385        let obscured = TextInputConfiguration::new().obscure_text(true);
1386        assert!(obscured.obscure_text);
1387        assert_eq!(obscured.smart_dashes_type, SmartDashesType::Disabled);
1388        assert_eq!(obscured.smart_quotes_type, SmartQuotesType::Disabled);
1389
1390        let restored = obscured.smart_dashes_type(SmartDashesType::Enabled);
1391        assert_eq!(restored.smart_dashes_type, SmartDashesType::Enabled);
1392    }
1393
1394    #[test]
1395    fn autofill_disabled_is_the_configuration_default() {
1396        let configuration = TextInputConfiguration::new();
1397        assert!(!configuration.autofill_configuration.enabled);
1398        assert_eq!(
1399            AutofillConfiguration::new("id", vec!["email".into()], TextEditingValue::EMPTY)
1400                .hint_text("email")
1401                .hint_text,
1402            Some("email".into())
1403        );
1404    }
1405}