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}