cranpose_ui/keyboard_options.rs
1//! Compose's `KeyboardOptions` and keyboard actions: which software keyboard
2//! a text field asks for, and what its action key does.
3
4use std::{cell::Cell, rc::Rc};
5
6/// The keys a software keyboard offers: Compose's `KeyboardType`.
7#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
8pub enum KeyboardType {
9 /// The platform's text keyboard.
10 #[default]
11 Text,
12 /// Letters, digits and symbols from ASCII only.
13 Ascii,
14 /// Digits.
15 Number,
16 /// Digits and the symbols of a phone number.
17 Phone,
18 /// Text keys for a web address.
19 Uri,
20 /// Text keys for an email address.
21 Email,
22 /// Text keys for a password: no suggestions, no learning from it.
23 Password,
24 /// Digits for a PIN: no suggestions, no learning from it.
25 NumberPassword,
26 /// Digits and a decimal separator.
27 Decimal,
28}
29
30impl KeyboardType {
31 /// Whether the keyboard enters a secret, which it neither corrects nor
32 /// learns from.
33 pub fn is_password(self) -> bool {
34 matches!(self, Self::Password | Self::NumberPassword)
35 }
36}
37
38/// Which letters a software keyboard capitalizes on its own: Compose's
39/// `KeyboardCapitalization`. Only text keyboards capitalize.
40#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
41pub enum KeyboardCapitalization {
42 #[default]
43 None,
44 /// Every character.
45 Characters,
46 /// The first letter of every word.
47 Words,
48 /// The first letter of every sentence.
49 Sentences,
50}
51
52/// The action key a software keyboard shows, which Enter also runs in a
53/// single-line field: Compose's `ImeAction`.
54#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
55pub enum ImeAction {
56 /// [`Done`](Self::Done) in a single-line field, [`None`](Self::None) in
57 /// a multi-line one.
58 #[default]
59 Default,
60 /// No action: Enter adds a line in a multi-line field and does nothing
61 /// in a single-line one.
62 None,
63 Go,
64 Search,
65 Send,
66 /// Moves focus to the previous field by default.
67 Previous,
68 /// Moves focus to the next field by default.
69 Next,
70 /// Hides the software keyboard by default.
71 Done,
72}
73
74impl ImeAction {
75 /// The action a field with these lines shows: [`Default`](Self::Default)
76 /// resolved, any other action as it is.
77 pub fn resolve(self, single_line: bool) -> Self {
78 match self {
79 Self::Default if single_line => Self::Done,
80 Self::Default => Self::None,
81 action => action,
82 }
83 }
84}
85
86/// What a text field holds, so autofill and password managers can offer to
87/// fill it: Compose's `ContentType`. Web and iOS pass it to the platform;
88/// Android does not fill text fields yet.
89#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
90pub enum ContentType {
91 Username,
92 Password,
93 /// A username the person is choosing.
94 NewUsername,
95 /// A password the person is choosing, for which a password manager can
96 /// suggest a strong one.
97 NewPassword,
98 EmailAddress,
99 PhoneNumber,
100 PersonFullName,
101 PostalAddress,
102 PostalCode,
103 CreditCardNumber,
104 /// A one-time code from a text message.
105 SmsOtpCode,
106}
107
108/// The software keyboard a text field asks for: Compose's `KeyboardOptions`.
109/// Platforms without a software keyboard ignore everything but the action,
110/// which Enter runs in a single-line field.
111#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
112pub struct KeyboardOptions {
113 pub keyboard_type: KeyboardType,
114 pub capitalization: KeyboardCapitalization,
115 /// Whether the keyboard corrects and suggests words. Password keyboards
116 /// never do.
117 pub auto_correct: bool,
118 pub ime_action: ImeAction,
119 /// What the field holds, for autofill. Compose sets this through the
120 /// field's semantics.
121 pub content_type: Option<ContentType>,
122}
123
124impl Default for KeyboardOptions {
125 fn default() -> Self {
126 Self {
127 keyboard_type: KeyboardType::Text,
128 capitalization: KeyboardCapitalization::None,
129 auto_correct: true,
130 ime_action: ImeAction::Default,
131 content_type: None,
132 }
133 }
134}
135
136impl KeyboardOptions {
137 /// Whether the keyboard corrects and suggests words: asked for, and not
138 /// a password keyboard.
139 pub fn corrects(&self) -> bool {
140 self.auto_correct && !self.keyboard_type.is_password()
141 }
142}
143
144/// What a keyboard action handler gets: the action the person ran, and a
145/// way to run that action's default behavior too, as Compose's
146/// `performDefaultAction` does.
147pub struct KeyboardActionScope {
148 action: ImeAction,
149 default_requested: Cell<bool>,
150}
151
152impl KeyboardActionScope {
153 /// The resolved action: never [`ImeAction::Default`].
154 pub fn action(&self) -> ImeAction {
155 self.action
156 }
157
158 /// Runs the action's default after the handler returns: Next and
159 /// Previous move focus, Done hides the software keyboard, and the other
160 /// actions do nothing.
161 pub fn perform_default_action(&self) {
162 self.default_requested.set(true);
163 }
164}
165
166/// Runs when the person presses a field's keyboard action key, or Enter in
167/// a single-line field. A field without a handler runs the action's
168/// default; a handler runs it only through
169/// [`KeyboardActionScope::perform_default_action`].
170#[derive(Clone)]
171pub struct KeyboardActionHandler(Rc<dyn Fn(&KeyboardActionScope)>);
172
173impl KeyboardActionHandler {
174 pub fn new(handler: impl Fn(&KeyboardActionScope) + 'static) -> Self {
175 Self(Rc::new(handler))
176 }
177}
178
179impl std::fmt::Debug for KeyboardActionHandler {
180 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
181 f.write_str("KeyboardActionHandler")
182 }
183}
184
185impl PartialEq for KeyboardActionHandler {
186 fn eq(&self, other: &Self) -> bool {
187 Rc::ptr_eq(&self.0, &other.0)
188 }
189}
190
191/// Runs `action`, resolved, through `handler` or else its default.
192pub(crate) fn run_keyboard_action(action: ImeAction, handler: Option<&KeyboardActionHandler>) {
193 let scope = KeyboardActionScope {
194 action,
195 default_requested: Cell::new(handler.is_none()),
196 };
197 if let Some(handler) = handler {
198 (handler.0)(&scope);
199 }
200 if scope.default_requested.get() {
201 perform_default_action(action);
202 }
203}
204
205fn perform_default_action(action: ImeAction) {
206 match action {
207 ImeAction::Next => crate::focus_order::request_focus_move(crate::FocusDirection::Next),
208 ImeAction::Previous => {
209 crate::focus_order::request_focus_move(crate::FocusDirection::Previous);
210 }
211 ImeAction::Done => crate::text_input_session::hide_software_keyboard(),
212 ImeAction::Default
213 | ImeAction::None
214 | ImeAction::Go
215 | ImeAction::Search
216 | ImeAction::Send => {}
217 }
218}