Skip to main content

frus_widgets/
ime.rs

1//! What the **software keyboard** should be, for the field that has focus.
2//!
3//! A phone-number field that opens a full QWERTY keyboard is a defect you feel on
4//! every use and never see in a screenshot. So is an email field with no `@` key, a
5//! form whose every action key says *Done* when the next thing to do is move to the
6//! next field, and — the one that matters most — a password field the keyboard
7//! believes is an ordinary sentence, learns into its personal dictionary, and offers
8//! back as a suggestion later, on somebody else's screen.
9//!
10//! [`KeyboardType`] says which keys; [`TextInputAction`] says what the action key does.
11//! Together they are [`Ime`], which [`Widget::ime`](crate::Widget::ime) hands to the
12//! platform when a field takes focus.
13//!
14//! ```ignore
15//! TextField::new(&app.phone).keyboard_type(KeyboardType::Phone)
16//! TextField::new(&app.query).action(TextInputAction::Search)
17//! ```
18//!
19//! # The numbers are Android's, and the tests are not
20//!
21//! The mapping to Android's `InputType` and `EditorInfo` bit fields lives here rather
22//! than in the platform layer, and it is not behind `cfg(android)`. Two reasons, and
23//! they are the same reason: **a mapping only exercised on a device is a mapping
24//! nobody checks**, and the input bridge's dex is *checked in* — rebuilding it needs
25//! the Android SDK, so the Java side must never have to change again to learn a new
26//! keyboard type. It receives two integers and sets them on the `EditorInfo`. Every
27//! type this file will ever grow is a change to this file alone.
28
29/// Which keys the software keyboard should offer.
30///
31/// The platform decides what that means: a phone keypad on Android and iOS, and
32/// nothing at all on a desktop, which has one keyboard and it is already open.
33#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
34pub enum KeyboardType {
35    /// Ordinary text, with suggestions and composition. The default.
36    #[default]
37    Text,
38    /// Text over several lines, where the action key is a newline.
39    Multiline,
40    /// Whole numbers.
41    Number,
42    /// Numbers with a decimal separator and a sign.
43    Decimal,
44    /// A telephone keypad: digits, `+`, `*`, `#`.
45    Phone,
46    /// An email address: an `@` and a `.` within reach, and no auto-capitalisation.
47    Email,
48    /// A URL: a `/` and a `.com` within reach.
49    Url,
50    /// A **secret**. The keyboard neither learns it nor suggests it, which is the
51    /// whole point and is not something the masking dots can do on their own.
52    Password,
53    /// A secret the field shows in the clear — a one-time code, a generated
54    /// passphrase being checked. Still never learned.
55    VisiblePassword,
56    /// A person's name: each word capitalised, no sentence casing.
57    Name,
58    /// A postal address.
59    StreetAddress,
60    /// A date or a time.
61    DateTime,
62    /// **No** software keyboard: the field is edited some other way — a picker, a
63    /// scanner, a hardware keyboard only.
64    None,
65}
66
67/// What the keyboard's action key does, and what it says.
68#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
69pub enum TextInputAction {
70    /// *Done*: finish editing and close the keyboard. The default.
71    #[default]
72    Done,
73    /// *Go*: follow what was typed, usually an address.
74    Go,
75    /// *Search*.
76    Search,
77    /// *Send*.
78    Send,
79    /// *Next*: move to the following field, keeping the keyboard open.
80    Next,
81    /// *Previous*: move back to the field before.
82    Previous,
83    /// A **newline**, for a field that takes more than one line. Not an action at
84    /// all — the key inserts a line break instead of doing anything.
85    Newline,
86    /// No action key behaviour at all; the platform picks.
87    Unspecified,
88}
89
90/// Which letters the software keyboard capitalises on its own.
91///
92/// A hint, not a rule: it is what the **keyboard** does to what it sends, and a reader
93/// who turns it off still types what they meant to. A field that must hold capitals
94/// wants [`TextField::input_filter`](crate::TextField::input_filter) as well, which
95/// works whatever the keyboard does and works on a desktop, where there is no keyboard
96/// to hint to.
97#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
98pub enum Capitalization {
99    /// Whatever the keyboard type already implies — sentences for ordinary text, words
100    /// for a name or an address, nothing for an email address. The default, and right
101    /// nearly always, since the type is the better description of the two.
102    #[default]
103    Auto,
104    /// Nothing. The reader capitalises what they mean to capitalise.
105    None,
106    /// The first letter of each sentence.
107    Sentences,
108    /// The first letter of each word.
109    Words,
110    /// Every letter.
111    Characters,
112}
113
114impl Capitalization {
115    /// The Android `InputType` bit for this, or zero for none.
116    const fn flag(self) -> i32 {
117        use input_type as t;
118        match self {
119            // `Auto` never reaches here; see [`Ime::android_input_type`].
120            Self::Auto | Self::None => 0,
121            Self::Sentences => t::TEXT_FLAG_CAP_SENTENCES,
122            Self::Words => t::TEXT_FLAG_CAP_WORDS,
123            Self::Characters => t::TEXT_FLAG_CAP_CHARACTERS,
124        }
125    }
126}
127
128/// The pair the platform is told when a field takes focus.
129#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
130pub struct Ime {
131    /// Which keys.
132    pub keyboard: KeyboardType,
133    /// What the action key does.
134    pub action: TextInputAction,
135    /// Which letters the keyboard capitalises by itself.
136    pub capitalization: Capitalization,
137}
138
139impl Ime {
140    /// A keyboard of `keyboard` whose action key is *Done*.
141    pub const fn new(keyboard: KeyboardType) -> Self {
142        Self {
143            keyboard,
144            action: TextInputAction::Done,
145            capitalization: Capitalization::Auto,
146        }
147    }
148
149    /// The same, capitalising differently from what the type implies.
150    pub const fn capitalization(mut self, capitalization: Capitalization) -> Self {
151        self.capitalization = capitalization;
152        self
153    }
154
155    /// The Android `InputType` bit field for this field: the type's, with the
156    /// capitalisation overruled where one was asked for.
157    ///
158    /// Only a **text** class capitalises. A number pad has no letters to capitalise and
159    /// the same bits mean other things there (`0x1000` is *signed* on a number class),
160    /// so asking a phone field for capitals would quietly turn on a minus key.
161    ///
162    /// [`Capitalization::Auto`] leaves the type's own bits alone, which is why the
163    /// existing types keep answering exactly what they always did.
164    pub const fn android_input_type(self) -> i32 {
165        use input_type as t;
166        let base = self.keyboard.android_input_type();
167        if base & t::CLASS_MASK != t::CLASS_TEXT {
168            return base;
169        }
170        match self.capitalization {
171            Capitalization::Auto => base,
172            other => (base & !t::CAP_MASK) | other.flag(),
173        }
174    }
175
176    /// The same, with a different action key.
177    pub const fn action(mut self, action: TextInputAction) -> Self {
178        self.action = action;
179        self
180    }
181}
182
183// ---------------------------------------------------------------------------
184// Android's numbers
185// ---------------------------------------------------------------------------
186
187/// `android.text.InputType`, the constants this file needs.
188mod input_type {
189    pub const CLASS_TEXT: i32 = 0x0000_0001;
190    pub const CLASS_NUMBER: i32 = 0x0000_0002;
191    pub const CLASS_PHONE: i32 = 0x0000_0003;
192    pub const CLASS_DATETIME: i32 = 0x0000_0004;
193
194    pub const CLASS_MASK: i32 = 0x0000_000f;
195
196    pub const TEXT_FLAG_CAP_CHARACTERS: i32 = 0x0000_1000;
197    pub const TEXT_FLAG_CAP_SENTENCES: i32 = 0x0000_4000;
198    pub const TEXT_FLAG_CAP_WORDS: i32 = 0x0000_2000;
199    /// Every capitalisation bit, so that asking for one can clear the others.
200    pub const CAP_MASK: i32 =
201        TEXT_FLAG_CAP_CHARACTERS | TEXT_FLAG_CAP_SENTENCES | TEXT_FLAG_CAP_WORDS;
202    pub const TEXT_FLAG_MULTI_LINE: i32 = 0x0002_0000;
203
204    pub const TEXT_VARIATION_URI: i32 = 0x0000_0010;
205    pub const TEXT_VARIATION_EMAIL_ADDRESS: i32 = 0x0000_0020;
206    pub const TEXT_VARIATION_PASSWORD: i32 = 0x0000_0080;
207    pub const TEXT_VARIATION_PERSON_NAME: i32 = 0x0000_0060;
208    pub const TEXT_VARIATION_POSTAL_ADDRESS: i32 = 0x0000_0050;
209    pub const TEXT_VARIATION_VISIBLE_PASSWORD: i32 = 0x0000_0090;
210
211    pub const NUMBER_FLAG_SIGNED: i32 = 0x0000_1000;
212    pub const NUMBER_FLAG_DECIMAL: i32 = 0x0000_2000;
213}
214
215/// `android.view.inputmethod.EditorInfo`, likewise.
216mod editor_info {
217    pub const IME_ACTION_UNSPECIFIED: i32 = 0;
218    pub const IME_ACTION_GO: i32 = 2;
219    pub const IME_ACTION_SEARCH: i32 = 3;
220    pub const IME_ACTION_SEND: i32 = 4;
221    pub const IME_ACTION_NEXT: i32 = 5;
222    pub const IME_ACTION_DONE: i32 = 6;
223    pub const IME_ACTION_PREVIOUS: i32 = 7;
224    pub const IME_FLAG_NO_FULLSCREEN: i32 = 0x0200_0000;
225}
226
227impl KeyboardType {
228    /// The Android `InputType` bit field for this keyboard.
229    ///
230    /// [`None`](Self::None) is `TYPE_NULL`, which is zero — the value that tells
231    /// Android there is nothing to type into. It is also what the bridge produced for
232    /// *every* field before it had an `InputType` at all, which is why a keyboard used
233    /// to arrive with no composition, no swipe and no CJK.
234    pub const fn android_input_type(self) -> i32 {
235        use input_type as t;
236        match self {
237            Self::Text => t::CLASS_TEXT | t::TEXT_FLAG_CAP_SENTENCES,
238            Self::Multiline => t::CLASS_TEXT | t::TEXT_FLAG_CAP_SENTENCES | t::TEXT_FLAG_MULTI_LINE,
239            Self::Number => t::CLASS_NUMBER,
240            Self::Decimal => t::CLASS_NUMBER | t::NUMBER_FLAG_DECIMAL | t::NUMBER_FLAG_SIGNED,
241            Self::Phone => t::CLASS_PHONE,
242            // No capitalisation flag: an address that arrives as `Someone@…` is an
243            // address that does not work, and the keyboard would supply the capital.
244            Self::Email => t::CLASS_TEXT | t::TEXT_VARIATION_EMAIL_ADDRESS,
245            Self::Url => t::CLASS_TEXT | t::TEXT_VARIATION_URI,
246            Self::Password => t::CLASS_TEXT | t::TEXT_VARIATION_PASSWORD,
247            Self::VisiblePassword => t::CLASS_TEXT | t::TEXT_VARIATION_VISIBLE_PASSWORD,
248            Self::Name => t::CLASS_TEXT | t::TEXT_VARIATION_PERSON_NAME | t::TEXT_FLAG_CAP_WORDS,
249            Self::StreetAddress => {
250                t::CLASS_TEXT | t::TEXT_VARIATION_POSTAL_ADDRESS | t::TEXT_FLAG_CAP_WORDS
251            }
252            Self::DateTime => t::CLASS_DATETIME,
253            Self::None => 0,
254        }
255    }
256
257    /// Whether the keyboard must never learn what is typed into it.
258    ///
259    /// Android infers this from the variation bits, so nothing else has to be sent —
260    /// but the question is worth asking in this vocabulary too, because it is the
261    /// reason [`Password`](Self::Password) exists as a *type* rather than as a way of
262    /// drawing dots.
263    pub const fn is_secret(self) -> bool {
264        matches!(self, Self::Password | Self::VisiblePassword)
265    }
266}
267
268impl TextInputAction {
269    /// The Android `imeOptions` bit field for this action key.
270    ///
271    /// `IME_FLAG_NO_FULLSCREEN` is on every one of them: the fullscreen editor Android
272    /// offers in landscape replaces the application's own field with a system one, and
273    /// a framework that draws its own text has nothing to gain and a screen to lose.
274    pub const fn android_ime_options(self) -> i32 {
275        use editor_info as e;
276        let action = match self {
277            Self::Done => e::IME_ACTION_DONE,
278            Self::Go => e::IME_ACTION_GO,
279            Self::Search => e::IME_ACTION_SEARCH,
280            Self::Send => e::IME_ACTION_SEND,
281            Self::Next => e::IME_ACTION_NEXT,
282            Self::Previous => e::IME_ACTION_PREVIOUS,
283            // A newline is not an action: the key has to fall through to the editor,
284            // and `UNSPECIFIED` with the multi-line input flag is how Android says so.
285            Self::Newline | Self::Unspecified => e::IME_ACTION_UNSPECIFIED,
286        };
287        action | e::IME_FLAG_NO_FULLSCREEN
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294
295    /// The pair a field with nothing said is what the bridge used to hardcode for
296    /// every field: sentence-cased text, a *Done* key, no fullscreen editor. Nothing
297    /// that says nothing changes.
298    #[test]
299    fn the_default_is_what_was_hardcoded_before() {
300        let ime = Ime::default();
301        assert_eq!(ime.keyboard, KeyboardType::Text);
302        assert_eq!(ime.action, TextInputAction::Done);
303        // TYPE_CLASS_TEXT | TYPE_TEXT_FLAG_CAP_SENTENCES
304        assert_eq!(ime.keyboard.android_input_type(), 1 | 0x4000);
305        // IME_ACTION_DONE | IME_FLAG_NO_FULLSCREEN
306        assert_eq!(ime.action.android_ime_options(), 6 | 0x0200_0000);
307    }
308
309    /// Asking for a capitalisation **replaces** the type's own rather than adding to it:
310    /// two capitalisation bits at once is a keyboard being told two things.
311    #[test]
312    fn a_capitalisation_replaces_the_types_own() {
313        const CAP_MASK: i32 = 0x0000_7000;
314        let bits = |c: Capitalization| Ime::new(KeyboardType::Text).capitalization(c);
315        // Ordinary text capitalises sentences by itself.
316        assert_eq!(
317            Ime::new(KeyboardType::Text).android_input_type() & CAP_MASK,
318            0x0000_4000
319        );
320        assert_eq!(
321            bits(Capitalization::Words).android_input_type() & CAP_MASK,
322            0x0000_2000
323        );
324        assert_eq!(
325            bits(Capitalization::Characters).android_input_type() & CAP_MASK,
326            0x0000_1000
327        );
328        assert_eq!(
329            bits(Capitalization::None).android_input_type() & CAP_MASK,
330            0
331        );
332        // And the rest of the field is untouched: still a text class.
333        assert_eq!(bits(Capitalization::None).android_input_type() & 0xf, 1);
334    }
335
336    /// `Auto` is the absence of an opinion, so every type answers exactly what it always
337    /// did — a name still capitalises words, an email address still capitalises nothing.
338    #[test]
339    fn auto_leaves_every_type_as_it_was() {
340        for keyboard in [
341            KeyboardType::Text,
342            KeyboardType::Multiline,
343            KeyboardType::Name,
344            KeyboardType::Email,
345            KeyboardType::Password,
346            KeyboardType::Number,
347            KeyboardType::Phone,
348        ] {
349            assert_eq!(
350                Ime::new(keyboard).android_input_type(),
351                keyboard.android_input_type(),
352                "{keyboard:?}"
353            );
354        }
355    }
356
357    /// Only a text class capitalises. `0x1000` is *signed* on a number class, so asking
358    /// a keypad for capitals would quietly turn on a minus key.
359    #[test]
360    fn a_keypad_is_never_told_to_capitalise() {
361        for keyboard in [
362            KeyboardType::Number,
363            KeyboardType::Decimal,
364            KeyboardType::Phone,
365            KeyboardType::DateTime,
366        ] {
367            let asked = Ime::new(keyboard).capitalization(Capitalization::Characters);
368            assert_eq!(
369                asked.android_input_type(),
370                keyboard.android_input_type(),
371                "{keyboard:?}"
372            );
373        }
374    }
375
376    /// Each type carries its own class. A phone field on a text class is a QWERTY
377    /// keyboard with a wrong label.
378    #[test]
379    fn each_type_names_its_own_class() {
380        const CLASS_MASK: i32 = 0x0000_000f;
381        let class = |k: KeyboardType| k.android_input_type() & CLASS_MASK;
382        assert_eq!(class(KeyboardType::Text), 1);
383        assert_eq!(class(KeyboardType::Multiline), 1);
384        assert_eq!(class(KeyboardType::Number), 2);
385        assert_eq!(class(KeyboardType::Decimal), 2);
386        assert_eq!(class(KeyboardType::Phone), 3);
387        assert_eq!(class(KeyboardType::DateTime), 4);
388        assert_eq!(class(KeyboardType::Email), 1);
389        assert_eq!(class(KeyboardType::Url), 1);
390    }
391
392    /// `None` is `TYPE_NULL`, and it is the only zero. A type that fell through to
393    /// zero by accident would silently be *no keyboard at all*, which is the one
394    /// failure a user cannot work around.
395    #[test]
396    fn only_none_is_null() {
397        let every = [
398            KeyboardType::Text,
399            KeyboardType::Multiline,
400            KeyboardType::Number,
401            KeyboardType::Decimal,
402            KeyboardType::Phone,
403            KeyboardType::Email,
404            KeyboardType::Url,
405            KeyboardType::Password,
406            KeyboardType::VisiblePassword,
407            KeyboardType::Name,
408            KeyboardType::StreetAddress,
409            KeyboardType::DateTime,
410        ];
411        for keyboard in every {
412            assert_ne!(
413                keyboard.android_input_type(),
414                0,
415                "{keyboard:?} must not be TYPE_NULL"
416            );
417        }
418        assert_eq!(KeyboardType::None.android_input_type(), 0);
419    }
420
421    /// A secret carries a password **variation**, which is what stops the keyboard
422    /// learning it. Masking dots are drawn by us and tell the keyboard nothing.
423    #[test]
424    fn a_secret_carries_a_password_variation() {
425        const VARIATION_MASK: i32 = 0x0000_0ff0;
426        assert_eq!(
427            KeyboardType::Password.android_input_type() & VARIATION_MASK,
428            0x80
429        );
430        assert_eq!(
431            KeyboardType::VisiblePassword.android_input_type() & VARIATION_MASK,
432            0x90
433        );
434        assert!(KeyboardType::Password.is_secret());
435        assert!(KeyboardType::VisiblePassword.is_secret());
436        assert!(!KeyboardType::Text.is_secret());
437    }
438
439    /// An email address is never auto-capitalised: `Someone@…` is an address that
440    /// does not work, and the keyboard would supply the capital unasked.
441    #[test]
442    fn an_email_is_not_capitalised() {
443        const CAP_MASK: i32 = 0x0000_7000;
444        assert_eq!(KeyboardType::Email.android_input_type() & CAP_MASK, 0);
445        assert_eq!(KeyboardType::Url.android_input_type() & CAP_MASK, 0);
446        assert_eq!(KeyboardType::Password.android_input_type() & CAP_MASK, 0);
447        // Ordinary prose is, and a name is capitalised per word rather than per
448        // sentence.
449        assert_ne!(KeyboardType::Text.android_input_type() & CAP_MASK, 0);
450        assert_eq!(
451            KeyboardType::Name.android_input_type() & CAP_MASK,
452            0x0000_2000
453        );
454    }
455
456    /// Multiline carries the multi-line flag, which is what lets the action key be a
457    /// newline at all.
458    #[test]
459    fn multiline_carries_the_multi_line_flag() {
460        const MULTI_LINE: i32 = 0x0002_0000;
461        assert_ne!(KeyboardType::Multiline.android_input_type() & MULTI_LINE, 0);
462        assert_eq!(KeyboardType::Text.android_input_type() & MULTI_LINE, 0);
463    }
464
465    /// A newline is `UNSPECIFIED`, not an action: the key has to reach the editor.
466    #[test]
467    fn a_newline_is_not_an_action() {
468        const ACTION_MASK: i32 = 0x0000_00ff;
469        assert_eq!(
470            TextInputAction::Newline.android_ime_options() & ACTION_MASK,
471            0
472        );
473        assert_eq!(
474            TextInputAction::Search.android_ime_options() & ACTION_MASK,
475            3
476        );
477        assert_eq!(TextInputAction::Next.android_ime_options() & ACTION_MASK, 5);
478    }
479
480    /// Every action refuses the fullscreen editor: it replaces the application's own
481    /// field with a system one, and a framework that draws its own text loses a screen
482    /// and gains nothing.
483    #[test]
484    fn no_action_accepts_the_fullscreen_editor() {
485        const NO_FULLSCREEN: i32 = 0x0200_0000;
486        for action in [
487            TextInputAction::Done,
488            TextInputAction::Go,
489            TextInputAction::Search,
490            TextInputAction::Send,
491            TextInputAction::Next,
492            TextInputAction::Previous,
493            TextInputAction::Newline,
494            TextInputAction::Unspecified,
495        ] {
496            assert_ne!(
497                action.android_ime_options() & NO_FULLSCREEN,
498                0,
499                "{action:?} must refuse the fullscreen editor"
500            );
501        }
502    }
503}