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    /// Starts configuration information for a text input control, with Dart's defaults.
656    /// Values left out are filled in by [`TextInputConfigurationBuilder::build`], as Dart's
657    /// constructor fills them.
658    pub fn builder() -> TextInputConfigurationBuilder {
659        TextInputConfigurationBuilder {
660            configuration: TextInputConfiguration {
661                view_id: None,
662                input_type: TextInputType::TEXT,
663                read_only: false,
664                obscure_text: false,
665                autocorrect: true,
666                smart_dashes_type: SmartDashesType::Enabled,
667                smart_quotes_type: SmartQuotesType::Enabled,
668                enable_suggestions: true,
669                enable_interactive_selection: true,
670                action_label: None,
671                input_action: TextInputAction::Done,
672                text_capitalization: TextCapitalization::None,
673                keyboard_appearance: Brightness::Light,
674                autofill_configuration: AutofillConfiguration::DISABLED,
675                enable_ime_personalized_learning: true,
676                allowed_mime_types: Vec::new(),
677                enable_delta_model: false,
678                hint_locales: Vec::new(),
679                enable_inline_prediction: None,
680            },
681            smart_dashes_type: None,
682            smart_quotes_type: None,
683        }
684    }
685
686    /// Dart `copyWith`: a builder holding this configuration's values, for fields to be
687    /// replaced.
688    pub fn copy_with(&self) -> TextInputConfigurationBuilder {
689        TextInputConfigurationBuilder {
690            configuration: self.clone(),
691            smart_dashes_type: Some(self.smart_dashes_type),
692            smart_quotes_type: Some(self.smart_quotes_type),
693        }
694    }
695}
696
697/// `TextInputConfigurationBuilder` collects a [`TextInputConfiguration`]'s values, as Dart's
698/// constructor takes them, and fills in the ones left out when it builds.
699#[derive(Clone, Debug)]
700pub struct TextInputConfigurationBuilder {
701    configuration: TextInputConfiguration,
702    smart_dashes_type: Option<SmartDashesType>,
703    smart_quotes_type: Option<SmartQuotesType>,
704}
705
706impl TextInputConfigurationBuilder {
707    /// Dart `TextInputConfiguration(viewId:)`.
708    pub fn view_id(mut self, view_id: i32) -> TextInputConfigurationBuilder {
709        self.configuration.view_id = Some(view_id);
710        self
711    }
712
713    /// Dart `TextInputConfiguration(inputType:)`.
714    pub fn input_type(mut self, input_type: TextInputType) -> TextInputConfigurationBuilder {
715        self.configuration.input_type = input_type;
716        self
717    }
718
719    /// Dart `TextInputConfiguration(readOnly:)`.
720    pub fn read_only(mut self, read_only: bool) -> TextInputConfigurationBuilder {
721        self.configuration.read_only = read_only;
722        self
723    }
724
725    /// Dart `TextInputConfiguration(obscureText:)`.
726    pub fn obscure_text(mut self, obscure_text: bool) -> TextInputConfigurationBuilder {
727        self.configuration.obscure_text = obscure_text;
728        self
729    }
730
731    /// Dart `TextInputConfiguration(autocorrect:)`.
732    pub fn autocorrect(mut self, autocorrect: bool) -> TextInputConfigurationBuilder {
733        self.configuration.autocorrect = autocorrect;
734        self
735    }
736
737    /// Dart `TextInputConfiguration(smartDashesType:)`.
738    pub fn smart_dashes_type(
739        mut self,
740        smart_dashes_type: SmartDashesType,
741    ) -> TextInputConfigurationBuilder {
742        self.smart_dashes_type = Some(smart_dashes_type);
743        self
744    }
745
746    /// Dart `TextInputConfiguration(smartQuotesType:)`.
747    pub fn smart_quotes_type(
748        mut self,
749        smart_quotes_type: SmartQuotesType,
750    ) -> TextInputConfigurationBuilder {
751        self.smart_quotes_type = Some(smart_quotes_type);
752        self
753    }
754
755    /// Dart `TextInputConfiguration(enableSuggestions:)`.
756    pub fn enable_suggestions(mut self, enable_suggestions: bool) -> TextInputConfigurationBuilder {
757        self.configuration.enable_suggestions = enable_suggestions;
758        self
759    }
760
761    /// Dart `TextInputConfiguration(enableInteractiveSelection:)`.
762    pub fn enable_interactive_selection(
763        mut self,
764        enable_interactive_selection: bool,
765    ) -> TextInputConfigurationBuilder {
766        self.configuration.enable_interactive_selection = enable_interactive_selection;
767        self
768    }
769
770    /// Dart `TextInputConfiguration(actionLabel:)`.
771    pub fn action_label(
772        mut self,
773        action_label: impl Into<String>,
774    ) -> TextInputConfigurationBuilder {
775        self.configuration.action_label = Some(action_label.into());
776        self
777    }
778
779    /// Dart `TextInputConfiguration(inputAction:)`.
780    pub fn input_action(mut self, input_action: TextInputAction) -> TextInputConfigurationBuilder {
781        self.configuration.input_action = input_action;
782        self
783    }
784
785    /// Dart `TextInputConfiguration(keyboardAppearance:)`.
786    pub fn keyboard_appearance(
787        mut self,
788        keyboard_appearance: Brightness,
789    ) -> TextInputConfigurationBuilder {
790        self.configuration.keyboard_appearance = keyboard_appearance;
791        self
792    }
793
794    /// Dart `TextInputConfiguration(textCapitalization:)`.
795    pub fn text_capitalization(
796        mut self,
797        text_capitalization: TextCapitalization,
798    ) -> TextInputConfigurationBuilder {
799        self.configuration.text_capitalization = text_capitalization;
800        self
801    }
802
803    /// Dart `TextInputConfiguration(autofillConfiguration:)`.
804    pub fn autofill_configuration(
805        mut self,
806        autofill_configuration: AutofillConfiguration,
807    ) -> TextInputConfigurationBuilder {
808        self.configuration.autofill_configuration = autofill_configuration;
809        self
810    }
811
812    /// Dart `TextInputConfiguration(enableIMEPersonalizedLearning:)`.
813    pub fn enable_ime_personalized_learning(
814        mut self,
815        enable_ime_personalized_learning: bool,
816    ) -> TextInputConfigurationBuilder {
817        self.configuration.enable_ime_personalized_learning = enable_ime_personalized_learning;
818        self
819    }
820
821    /// Dart `TextInputConfiguration(allowedMimeTypes:)`.
822    pub fn allowed_mime_types(
823        mut self,
824        allowed_mime_types: Vec<String>,
825    ) -> TextInputConfigurationBuilder {
826        self.configuration.allowed_mime_types = allowed_mime_types;
827        self
828    }
829
830    /// Dart `TextInputConfiguration(enableDeltaModel:)`.
831    pub fn enable_delta_model(mut self, enable_delta_model: bool) -> TextInputConfigurationBuilder {
832        self.configuration.enable_delta_model = enable_delta_model;
833        self
834    }
835
836    /// Dart `TextInputConfiguration(hintLocales:)`.
837    pub fn hint_locales(mut self, hint_locales: Vec<Locale>) -> TextInputConfigurationBuilder {
838        self.configuration.hint_locales = hint_locales;
839        self
840    }
841
842    /// Dart `TextInputConfiguration(enableInlinePrediction:)`.
843    pub fn enable_inline_prediction(
844        mut self,
845        enable_inline_prediction: bool,
846    ) -> TextInputConfigurationBuilder {
847        self.configuration.enable_inline_prediction = Some(enable_inline_prediction);
848        self
849    }
850
851    /// `build` makes the configuration, turning smart dashes and quotes off for obscured text
852    /// unless they were given, as Dart's constructor does.
853    pub fn build(self) -> TextInputConfiguration {
854        let mut configuration = self.configuration;
855        let obscure_text = configuration.obscure_text;
856        configuration.smart_dashes_type = self.smart_dashes_type.unwrap_or(if obscure_text {
857            SmartDashesType::Disabled
858        } else {
859            SmartDashesType::Enabled
860        });
861        configuration.smart_quotes_type = self.smart_quotes_type.unwrap_or(if obscure_text {
862            SmartQuotesType::Disabled
863        } else {
864            SmartQuotesType::Enabled
865        });
866        configuration
867    }
868}
869
870impl Default for TextInputConfiguration {
871    fn default() -> TextInputConfiguration {
872        TextInputConfiguration::builder().build()
873    }
874}
875
876impl fmt::Display for TextInputConfiguration {
877    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
878        let mut parts = vec![
879            format!("inputType: {}", self.input_type),
880            format!("readOnly: {}", self.read_only),
881            format!("obscureText: {}", self.obscure_text),
882            format!("autocorrect: {}", self.autocorrect),
883            format!("smartDashesType: {:?}", self.smart_dashes_type),
884            format!("smartQuotesType: {:?}", self.smart_quotes_type),
885            format!("enableSuggestions: {}", self.enable_suggestions),
886            format!(
887                "enableInteractiveSelection: {}",
888                self.enable_interactive_selection
889            ),
890            format!("inputAction: {:?}", self.input_action),
891            format!("keyboardAppearance: {:?}", self.keyboard_appearance),
892            format!("textCapitalization: {:?}", self.text_capitalization),
893            format!("autofillConfiguration: {}", self.autofill_configuration),
894            format!(
895                "enableIMEPersonalizedLearning: {}",
896                self.enable_ime_personalized_learning
897            ),
898            format!("allowedMimeTypes: {:?}", self.allowed_mime_types),
899            format!("enableDeltaModel: {}", self.enable_delta_model),
900        ];
901        if let Some(view_id) = self.view_id {
902            parts.insert(0, format!("viewId: {view_id}"));
903        }
904        if let Some(action_label) = &self.action_label {
905            parts.insert(
906                parts
907                    .iter()
908                    .position(|p| p.starts_with("inputAction:"))
909                    .unwrap_or(parts.len()),
910                format!("actionLabel: {action_label}"),
911            );
912        }
913        if !self.hint_locales.is_empty() {
914            parts.push(format!("hintLocales: {:?}", self.hint_locales));
915        }
916        if let Some(enable_inline_prediction) = self.enable_inline_prediction {
917            parts.push(format!(
918                "enableInlinePrediction: {enable_inline_prediction}"
919            ));
920        }
921        write!(f, "TextInputConfiguration({})", parts.join(", "))
922    }
923}
924
925/// The state of a "floating cursor" drag on an iOS soft keyboard.
926///
927/// The "floating cursor" cursor-positioning mode is an iOS feature used to
928/// precisely position the caret in some editable text using certain touch
929/// gestures. As an example, when the user long-presses the spacebar on the iOS
930/// virtual keyboard, iOS enters floating cursor mode where the whole keyboard
931/// becomes a trackpad. In this mode, there are two visible cursors. One, the
932/// floating cursor, hovers over the text, following the user's horizontal
933/// movements exactly and snapping to lines vertically. The other, the
934/// placeholder cursor, is a "shadow" that also snaps to the actual location
935/// where the cursor will go horizontally when the user releases the trackpad.
936///
937/// The floating cursor renders over the text field, while the placeholder
938/// cursor is a faint shadow of the cursor rendered in the text field in the
939/// location between characters where the cursor will drop into when released.
940/// The placeholder cursor is a faint vertical bar, while the floating cursor
941/// has the same appearance as a normal cursor (a blue vertical bar).
942///
943/// This feature works out-of-the-box with Flutter. Support is built into
944/// `EditableText`.
945///
946/// See also:
947///
948///  * `EditableText.backgroundCursorColor`, which configures the color of the
949///    placeholder cursor while the floating cursor is being dragged.
950#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
951pub enum FloatingCursorDragState {
952    /// A user has just activated a floating cursor by long pressing on the
953    /// spacebar.
954    Start,
955
956    /// A user is dragging a floating cursor.
957    Update,
958
959    /// A user has lifted their finger off the screen after using a floating
960    /// cursor.
961    End,
962}
963
964/// The current state and position of the floating cursor.
965///
966/// See also:
967///
968///  * [`FloatingCursorDragState`], which explains the floating cursor feature in
969///    detail.
970#[derive(Clone, Copy, Debug, PartialEq)]
971pub struct RawFloatingCursorPoint {
972    /// The raw position of the floating cursor as determined by the iOS sdk.
973    pub offset: Option<Offset>,
974    /// Represents the starting location when initiating a floating cursor via long press.
975    /// This is a tuple where the first item is the local offset and the second item is the new caret position.
976    /// This is only non-null when a floating cursor is started.
977    pub start_location: Option<(Offset, TextPosition)>,
978    /// The state of the floating cursor.
979    pub state: FloatingCursorDragState,
980}
981
982impl RawFloatingCursorPoint {
983    /// Creates information for setting the position and state of a floating
984    /// cursor.
985    ///
986    /// [`state`](Self::state) must not be null and [`offset`](Self::offset) must not be null if the state is
987    /// [`FloatingCursorDragState::Update`].
988    pub fn new(state: FloatingCursorDragState) -> RawFloatingCursorPoint {
989        let point = RawFloatingCursorPoint {
990            offset: None,
991            start_location: None,
992            state,
993        };
994        if state == FloatingCursorDragState::Update {
995            point
996        } else {
997            point.assemble()
998        }
999    }
1000
1001    /// Dart `RawFloatingCursorPoint(offset:)`.
1002    pub fn offset(mut self, offset: Offset) -> RawFloatingCursorPoint {
1003        self.offset = Some(offset);
1004        self.assemble()
1005    }
1006
1007    /// Dart `RawFloatingCursorPoint(startLocation:)`.
1008    pub fn start_location(
1009        mut self,
1010        start_location: (Offset, TextPosition),
1011    ) -> RawFloatingCursorPoint {
1012        self.start_location = Some(start_location);
1013        self
1014    }
1015
1016    fn assemble(self) -> RawFloatingCursorPoint {
1017        debug_assert!(
1018            self.state != FloatingCursorDragState::Update || self.offset.is_some(),
1019            "offset must not be null if the state is Update"
1020        );
1021        self
1022    }
1023}
1024
1025/// The current text, selection, and composing state for editing a run of text.
1026#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1027pub struct TextEditingValue {
1028    /// The current text being edited.
1029    pub text: String,
1030    /// The range of text that is currently selected.
1031    ///
1032    /// When [`selection`](Self::selection) is a [`TextSelection`] that has the same non-negative
1033    /// `baseOffset` and `extentOffset`, the [`selection`](Self::selection) property represents the
1034    /// caret position.
1035    ///
1036    /// If the current [`selection`](Self::selection) has a negative `baseOffset` or `extentOffset`,
1037    /// then the text currently does not have a selection or a caret location, and
1038    /// most text editing operations that rely on the current selection (for
1039    /// instance, insert a character at the caret location) will do nothing.
1040    pub selection: TextSelection,
1041    /// The range of text that is still being composed.
1042    ///
1043    /// Composing regions are created by input methods (IMEs) to indicate the text
1044    /// within a certain range is provisional. For instance, the Android Gboard
1045    /// app's English keyboard puts the current word under the caret into a
1046    /// composing region to indicate the word is subject to autocorrect or
1047    /// prediction changes.
1048    ///
1049    /// Composing regions can also be used for performing multistage input, which
1050    /// is typically used by IMEs designed for phonetic keyboard to enter
1051    /// ideographic symbols. As an example, many CJK keyboards require the user to
1052    /// enter a Latin alphabet sequence and then convert it to CJK characters. On
1053    /// iOS, the default software keyboards do not have a dedicated view to show
1054    /// the unfinished Latin sequence, so it's displayed directly in the text
1055    /// field, inside of a composing region.
1056    ///
1057    /// On iOS 17 and later, the composing region can also be used
1058    /// to display inline text predictions. The user can accept the
1059    /// predicted text by tapping the Space bar.
1060    ///
1061    /// The composing region should typically only be changed by the IME, or the
1062    /// user via interacting with the IME.
1063    ///
1064    /// If the range represented by this property is [`TextRange::EMPTY`], then the
1065    /// text is not currently being composed.
1066    pub composing: TextRange,
1067}
1068
1069impl TextEditingValue {
1070    /// A value that corresponds to the empty string with no selection and no composing range.
1071    pub const EMPTY: TextEditingValue = TextEditingValue {
1072        text: String::new(),
1073        selection: TextSelection::collapsed(-1, TextAffinity::Downstream),
1074        composing: TextRange::EMPTY,
1075    };
1076
1077    /// Creates information for editing a run of text.
1078    ///
1079    /// The selection and composing range must be within the text. This is not
1080    /// checked during construction, and must be guaranteed by the caller.
1081    ///
1082    /// The default value of [`selection`](Self::selection) is `TextSelection::collapsed(-1, …)`.
1083    /// This indicates that there is no selection at all.
1084    pub fn new() -> TextEditingValue {
1085        TextEditingValue::EMPTY
1086    }
1087
1088    /// Dart `TextEditingValue(text:)`.
1089    pub fn text(mut self, text: impl Into<String>) -> TextEditingValue {
1090        self.text = text.into();
1091        self
1092    }
1093
1094    /// Dart `TextEditingValue(selection:)`.
1095    pub fn selection(mut self, selection: TextSelection) -> TextEditingValue {
1096        self.selection = selection;
1097        self
1098    }
1099
1100    /// Dart `TextEditingValue(composing:)`.
1101    pub fn composing(mut self, composing: TextRange) -> TextEditingValue {
1102        self.composing = composing;
1103        self
1104    }
1105
1106    /// Creates a copy of this value but with the given fields replaced with the new values.
1107    pub fn copy_with(&self) -> TextEditingValue {
1108        self.clone()
1109    }
1110
1111    /// Whether the [`composing`](Self::composing) range is a valid range within [`text`](Self::text).
1112    ///
1113    /// Returns true if and only if the [`composing`](Self::composing) range is normalized, its start
1114    /// is greater than or equal to 0, and its end is less than or equal to
1115    /// [`text`](Self::text)'s length.
1116    ///
1117    /// If this property is false while the [`composing`](Self::composing) range's `isValid` is true,
1118    /// it usually indicates the current [`composing`](Self::composing) range is invalid because of a
1119    /// programming error.
1120    pub fn is_composing_range_valid(&self) -> bool {
1121        self.composing.is_valid()
1122            && self.composing.is_normalized()
1123            && self.composing.end <= utf16_len(&self.text)
1124    }
1125
1126    /// Returns a new [`TextEditingValue`], which is this [`TextEditingValue`] with
1127    /// its [`text`](Self::text) partially replaced by the `replacement_string`.
1128    ///
1129    /// The `replacement_range` parameter specifies the range of the
1130    /// [`text`](Self::text) that needs to be replaced.
1131    ///
1132    /// The `replacement_string` parameter specifies the string to replace the
1133    /// given range of text with.
1134    ///
1135    /// This method also adjusts the selection range and the composing range of the
1136    /// resulting [`TextEditingValue`], such that they point to the same substrings
1137    /// as the corresponding ranges in the original [`TextEditingValue`]. For
1138    /// example, if the original [`TextEditingValue`] is "Hello world" with the word
1139    /// "world" selected, replacing "Hello" with a different string using this
1140    /// method will not change the selected word.
1141    ///
1142    /// This method does nothing if the given `replacement_range` is not
1143    /// [`TextRange::is_valid`].
1144    pub fn replaced(
1145        &self,
1146        replacement_range: impl Into<TextRange>,
1147        replacement_string: &str,
1148    ) -> TextEditingValue {
1149        let replacement_range = replacement_range.into();
1150        if !replacement_range.is_valid() {
1151            return self.clone();
1152        }
1153        debug_assert!(replacement_range.is_normalized());
1154        let new_text = format!(
1155            "{}{}{}",
1156            replacement_range.text_before(&self.text),
1157            replacement_string,
1158            replacement_range.text_after(&self.text),
1159        );
1160        if replacement_range.end - replacement_range.start == utf16_len(replacement_string) {
1161            return self.copy_with().text(new_text);
1162        }
1163
1164        let replacement_len = utf16_len(replacement_string);
1165        let adjust_index = |original_index: i32| {
1166            // The length added by adding the replacementString.
1167            let replaced_length = if original_index <= replacement_range.start
1168                && original_index < replacement_range.end
1169            {
1170                0
1171            } else {
1172                replacement_len
1173            };
1174            // The length removed by removing the replacementRange.
1175            let removed_length = original_index
1176                .clamp(replacement_range.start, replacement_range.end)
1177                - replacement_range.start;
1178            original_index + replaced_length - removed_length
1179        };
1180
1181        let adjusted_selection = TextSelection::new(
1182            adjust_index(self.selection.base_offset),
1183            adjust_index(self.selection.extent_offset),
1184        );
1185        let adjusted_composing = TextRange::new(
1186            adjust_index(self.composing.start),
1187            adjust_index(self.composing.end),
1188        );
1189        debug_assert!(text_range_is_valid(adjusted_selection.range(), &new_text));
1190        debug_assert!(text_range_is_valid(adjusted_composing, &new_text));
1191        TextEditingValue::new()
1192            .text(new_text)
1193            .selection(adjusted_selection)
1194            .composing(adjusted_composing)
1195    }
1196}
1197
1198impl Default for TextEditingValue {
1199    fn default() -> TextEditingValue {
1200        TextEditingValue::new()
1201    }
1202}
1203
1204impl fmt::Display for TextEditingValue {
1205    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1206        write!(
1207            f,
1208            "TextEditingValue(text: \u{2524}{}\u{251C}, selection: {}, composing: {:?})",
1209            self.text, self.selection, self.composing
1210        )
1211    }
1212}
1213
1214/// Dart `String.length`: UTF-16 code units.
1215fn utf16_len(text: &str) -> i32 {
1216    text.encode_utf16().count() as i32
1217}
1218
1219/// Verify that the given range is within the text.
1220///
1221/// The verification can't be perform during the constructor of
1222/// [`TextEditingValue`], which are `const` and are allowed to retrieve
1223/// properties of [`TextRange`]s. [`TextEditingValue`] should perform this
1224/// wherever it is building other values (such as toJson) or is built in a
1225/// non-const way (such as fromJson).
1226fn text_range_is_valid(range: TextRange, text: &str) -> bool {
1227    if range.start == -1 && range.end == -1 {
1228        return true;
1229    }
1230    let len = utf16_len(text);
1231    debug_assert!(
1232        range.start >= 0 && range.start <= len,
1233        "Range start {} is out of text of length {len}",
1234        range.start
1235    );
1236    debug_assert!(
1237        range.end >= 0 && range.end <= len,
1238        "Range end {} is out of text of length {len}",
1239        range.end
1240    );
1241    true
1242}
1243
1244#[cfg(test)]
1245mod tests {
1246    use super::*;
1247
1248    const TEST_TEXT: &str = "From a false proposition, anything follows.";
1249
1250    #[test]
1251    fn replaced_deletes_a_selection() {
1252        let selection = TextSelection::new(5, 13);
1253        assert_eq!(
1254            TextEditingValue::new()
1255                .text(TEST_TEXT)
1256                .selection(selection)
1257                .replaced(selection, ""),
1258            TextEditingValue::new()
1259                .text("From proposition, anything follows.")
1260                .selection(TextSelection::collapsed(5, TextAffinity::Downstream))
1261        );
1262    }
1263
1264    #[test]
1265    fn replaced_deletes_a_reversed_selection() {
1266        let selection = TextSelection::new(13, 5);
1267        assert_eq!(
1268            TextEditingValue::new()
1269                .text(TEST_TEXT)
1270                .selection(selection)
1271                .replaced(selection, ""),
1272            TextEditingValue::new()
1273                .text("From proposition, anything follows.")
1274                .selection(TextSelection::collapsed(5, TextAffinity::Downstream))
1275        );
1276    }
1277
1278    #[test]
1279    fn replaced_inserts_at_the_caret() {
1280        let selection = TextSelection::collapsed(5, TextAffinity::Downstream);
1281        assert_eq!(
1282            TextEditingValue::new()
1283                .text(TEST_TEXT)
1284                .selection(selection)
1285                .replaced(selection, "AA"),
1286            TextEditingValue::new()
1287                .text("From AAa false proposition, anything follows.")
1288                .selection(TextSelection::collapsed(7, TextAffinity::Downstream))
1289        );
1290    }
1291
1292    #[test]
1293    fn replaced_before_selection_shifts_the_range() {
1294        let selection = TextSelection::new(13, 5);
1295        assert_eq!(
1296            TextEditingValue::new()
1297                .text(TEST_TEXT)
1298                .selection(selection)
1299                .replaced(TextRange::new(4, 5), "AA"),
1300            TextEditingValue::new()
1301                .text("FromAAa false proposition, anything follows.")
1302                .selection(TextSelection::new(14, 6))
1303        );
1304    }
1305
1306    #[test]
1307    fn replaced_after_selection_leaves_the_range() {
1308        let selection = TextSelection::new(13, 5);
1309        assert_eq!(
1310            TextEditingValue::new()
1311                .text(TEST_TEXT)
1312                .selection(selection)
1313                .replaced(TextRange::new(13, 14), "AA"),
1314            TextEditingValue::new()
1315                .text("From a false AAroposition, anything follows.")
1316                .selection(selection)
1317        );
1318    }
1319
1320    #[test]
1321    fn replaced_inside_selection_start_boundary() {
1322        let selection = TextSelection::new(13, 5);
1323        assert_eq!(
1324            TextEditingValue::new()
1325                .text(TEST_TEXT)
1326                .selection(selection)
1327                .replaced(TextRange::new(5, 6), "AA"),
1328            TextEditingValue::new()
1329                .text("From AA false proposition, anything follows.")
1330                .selection(TextSelection::new(14, 5))
1331        );
1332    }
1333
1334    #[test]
1335    fn replaced_inside_selection_end_boundary() {
1336        let selection = TextSelection::new(13, 5);
1337        assert_eq!(
1338            TextEditingValue::new()
1339                .text(TEST_TEXT)
1340                .selection(selection)
1341                .replaced(TextRange::new(12, 13), "AA"),
1342            TextEditingValue::new()
1343                .text("From a falseAAproposition, anything follows.")
1344                .selection(TextSelection::new(14, 5))
1345        );
1346    }
1347
1348    #[test]
1349    fn replaced_deletes_after_selection() {
1350        let selection = TextSelection::new(13, 5);
1351        assert_eq!(
1352            TextEditingValue::new()
1353                .text(TEST_TEXT)
1354                .selection(selection)
1355                .replaced(TextRange::new(13, 14), ""),
1356            TextEditingValue::new()
1357                .text("From a false roposition, anything follows.")
1358                .selection(selection)
1359        );
1360    }
1361
1362    #[test]
1363    fn replaced_deletes_inside_selection_start_boundary() {
1364        let selection = TextSelection::new(13, 5);
1365        assert_eq!(
1366            TextEditingValue::new()
1367                .text(TEST_TEXT)
1368                .selection(selection)
1369                .replaced(TextRange::new(5, 6), ""),
1370            TextEditingValue::new()
1371                .text("From  false proposition, anything follows.")
1372                .selection(TextSelection::new(12, 5))
1373        );
1374    }
1375
1376    #[test]
1377    fn replaced_deletes_inside_selection_end_boundary() {
1378        let selection = TextSelection::new(13, 5);
1379        assert_eq!(
1380            TextEditingValue::new()
1381                .text(TEST_TEXT)
1382                .selection(selection)
1383                .replaced(TextRange::new(12, 13), ""),
1384            TextEditingValue::new()
1385                .text("From a falseproposition, anything follows.")
1386                .selection(TextSelection::new(12, 5))
1387        );
1388    }
1389
1390    #[test]
1391    fn empty_prints_like_dart() {
1392        assert_eq!(
1393            TextEditingValue::EMPTY.to_string(),
1394            format!(
1395                "TextEditingValue(text: \u{2524}\u{251C}, selection: {}, composing: {:?})",
1396                TextSelection::collapsed(-1, TextAffinity::Downstream),
1397                TextRange::EMPTY
1398            )
1399        );
1400    }
1401
1402    #[test]
1403    fn number_with_options_defaults_match_dart() {
1404        assert_eq!(TextInputType::NUMBER.signed, Some(false));
1405        assert_eq!(
1406            TextInputType::number_with_options()
1407                .signed(true)
1408                .decimal(true),
1409            TextInputType {
1410                index: 2,
1411                signed: Some(true),
1412                decimal: Some(true),
1413                password: Some(false),
1414            }
1415        );
1416    }
1417
1418    #[test]
1419    fn obscure_text_disables_smart_punctuation_unless_overridden() {
1420        let obscured = TextInputConfiguration::builder().obscure_text(true).build();
1421        assert!(obscured.obscure_text);
1422        assert_eq!(obscured.smart_dashes_type, SmartDashesType::Disabled);
1423        assert_eq!(obscured.smart_quotes_type, SmartQuotesType::Disabled);
1424
1425        let given_first = TextInputConfiguration::builder()
1426            .smart_dashes_type(SmartDashesType::Enabled)
1427            .obscure_text(true)
1428            .build();
1429        assert_eq!(given_first.smart_dashes_type, SmartDashesType::Enabled);
1430    }
1431
1432    #[test]
1433    fn a_copy_keeps_the_smart_punctuation_it_was_made_with() {
1434        let copy = TextInputConfiguration::default()
1435            .copy_with()
1436            .obscure_text(true)
1437            .build();
1438        assert_eq!(copy.smart_dashes_type, SmartDashesType::Enabled);
1439        assert_eq!(copy.smart_quotes_type, SmartQuotesType::Enabled);
1440    }
1441
1442    #[test]
1443    fn autofill_disabled_is_the_configuration_default() {
1444        let configuration = TextInputConfiguration::default();
1445        assert!(!configuration.autofill_configuration.enabled);
1446        assert_eq!(
1447            AutofillConfiguration::new("id", vec!["email".into()], TextEditingValue::EMPTY)
1448                .hint_text("email")
1449                .hint_text,
1450            Some("email".into())
1451        );
1452    }
1453}