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}