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}