Skip to main content

zest_widget/widget/
keyboard.rs

1//! On-screen keyboard modeled on LVGL's `lv_keyboard`.
2//!
3//! The keyboard has four keymaps ([`KeyboardMode`]) — lowercase text,
4//! uppercase text, special symbols, and a numeric pad — mirroring LVGL's
5//! `LV_KEYBOARD_MODE_*`. The mode keys (`1#`, `ABC`/`abc`) switch the active
6//! keymap *in place*: uppercase is a sticky mode like LVGL's, not a one-shot
7//! shift. Character keys emit [`KeyAction::Char`]; the control keys emit
8//! backspace, newline, cursor-left / cursor-right, OK ([`KeyAction::Ready`]),
9//! hide ([`KeyAction::Cancel`]), and — for a password field — a reveal toggle
10//! ([`KeyAction::ToggleReveal`]).
11//!
12//! In the transient widget model the *host* owns the current
13//! [`KeyboardMode`] (and the edited text). Each frame `view()` builds a fresh
14//! `Keyboard` from the current mode, and the host's `update` applies each
15//! [`KeyAction`] — inserting / deleting characters, moving the cursor, and
16//! switching mode on [`KeyAction::Mode`]. Pair it with a
17//! [`TextArea`](super::text_area::TextArea) for an LVGL keyboard+textarea
18//! setup; the built-in preview field is off by default (enable it with
19//! [`show_field`](Keyboard::show_field) for a standalone keyboard).
20
21use super::{Widget, button::Button, column::Column, row::Row};
22use alloc::{
23    string::{String, ToString},
24    vec,
25    vec::Vec,
26};
27use embedded_graphics::{
28    pixelcolor::PixelColor, prelude::*, primitives::Rectangle, text::Alignment,
29};
30use zest_core::{Constraints, Length, RenderError, Renderer, TouchPhase};
31use zest_theme::{ButtonClass, Theme};
32
33/// Reserved height (px) for the optional title + preview field.
34const FIELD_H: u32 = 70;
35/// Width (px) of the password Show/Hide toggle button in the preview field.
36const SHOW_W: u32 = 56;
37
38/// Keyboard keymap, mirroring LVGL's `lv_keyboard_mode_t`.
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub enum KeyboardMode {
41    /// Lowercase QWERTY letters.
42    TextLower,
43    /// Uppercase QWERTY letters.
44    TextUpper,
45    /// Special symbols and punctuation.
46    Special,
47    /// Numeric pad.
48    Number,
49}
50
51/// An action emitted by a key press. The host applies it in `update`.
52#[derive(Clone, Copy, Debug, PartialEq, Eq)]
53pub enum KeyAction {
54    /// Insert this character at the cursor.
55    Char(char),
56    /// Delete the character before the cursor.
57    Backspace,
58    /// Insert a line break (or submit, for single-line fields).
59    Newline,
60    /// Move the cursor one character left.
61    CursorLeft,
62    /// Move the cursor one character right.
63    CursorRight,
64    /// Switch to a different keymap. The host stores the new
65    /// [`KeyboardMode`] and passes it back next frame.
66    Mode(KeyboardMode),
67    /// Accept / confirm (LVGL `LV_EVENT_READY`).
68    Ready,
69    /// Hide / cancel (LVGL `LV_EVENT_CANCEL`).
70    Cancel,
71    /// Tapped the password field's Show/Hide button. The host flips its
72    /// `reveal` flag and passes it back via [`Keyboard::reveal`].
73    ToggleReveal,
74}
75
76/// A single key in a keymap row: a label, the action it emits, and a flex
77/// weight (control keys are wider than character keys).
78struct Key {
79    label: String,
80    action: KeyAction,
81    weight: u32,
82}
83
84/// A character key (flex weight 1).
85fn ch(c: char) -> Key {
86    Key {
87        label: c.to_string(),
88        action: KeyAction::Char(c),
89        weight: 1,
90    }
91}
92
93/// A control key with an explicit label and flex weight.
94fn ctrl(label: &str, action: KeyAction, weight: u32) -> Key {
95    Key {
96        label: label.to_string(),
97        action,
98        weight,
99    }
100}
101
102/// The shared bottom row for the text/special keymaps:
103/// hide | cursor-left | space | cursor-right | OK.
104fn bottom_row() -> Vec<Key> {
105    vec![
106        ctrl("▼", KeyAction::Cancel, 2),
107        ctrl("←", KeyAction::CursorLeft, 1),
108        ctrl("space", KeyAction::Char(' '), 6),
109        ctrl("→", KeyAction::CursorRight, 1),
110        ctrl("OK", KeyAction::Ready, 2),
111    ]
112}
113
114/// On-screen keyboard. Built fresh each frame from the host-owned
115/// [`KeyboardMode`].
116pub struct Keyboard<'a, C: PixelColor, M: Clone> {
117    bounds: Rectangle,
118    title: String,
119    input: String,
120    is_password: bool,
121    show_field: bool,
122    reveal: bool,
123    keys: Column<'a, C, M>,
124    width: Length,
125    height: Length,
126    /// Host message emitted when the password Show/Hide button is tapped.
127    toggle_reveal: M,
128}
129
130impl<'a, C: PixelColor + 'a, M: Clone + 'a> Keyboard<'a, C, M> {
131    /// Construct a keyboard for `mode`. `on_action` translates each
132    /// [`KeyAction`] into the host's message type. Position and size are
133    /// assigned by the parent via `arrange`.
134    pub fn new<F>(mode: KeyboardMode, on_action: F) -> Self
135    where
136        F: Fn(KeyAction) -> M + Copy + 'a,
137    {
138        let toggle_reveal = on_action(KeyAction::ToggleReveal);
139        let keys = Self::build_keys(mode, on_action);
140        Self {
141            bounds: Rectangle::zero(),
142            title: String::new(),
143            input: String::new(),
144            is_password: false,
145            show_field: false,
146            reveal: false,
147            keys,
148            width: Length::Fill,
149            height: Length::Fill,
150            toggle_reveal,
151        }
152    }
153
154    /// Title shown above the optional preview field.
155    #[must_use]
156    pub fn title(mut self, title: impl Into<String>) -> Self {
157        self.title = title.into();
158        self
159    }
160
161    /// Text shown in the optional preview field.
162    #[must_use]
163    pub fn input(mut self, input: impl Into<String>) -> Self {
164        self.input = input.into();
165        self
166    }
167
168    /// Mask the preview field's text as `*` and add a Show/Hide
169    /// toggle to reveal it. Only takes effect with
170    /// [`show_field`](Self::show_field) on.
171    #[must_use]
172    pub fn is_password(mut self, is_password: bool) -> Self {
173        self.is_password = is_password;
174        self
175    }
176
177    /// Reveal the password text (host-owned). Tapping the Show/Hide
178    /// button emits [`KeyAction::ToggleReveal`]; the host flips its flag and
179    /// passes it back here so the field unmasks.
180    #[must_use]
181    pub fn reveal(mut self, reveal: bool) -> Self {
182        self.reveal = reveal;
183        self
184    }
185
186    /// Show the built-in title + preview field at the top (default
187    /// `false`). Leave off when pairing with a
188    /// [`TextArea`](super::text_area::TextArea); turn on for a standalone
189    /// keyboard that displays its own input.
190    #[must_use]
191    pub fn show_field(mut self, show: bool) -> Self {
192        self.show_field = show;
193        self
194    }
195
196    /// Width sizing intent.
197    #[must_use]
198    pub fn width(mut self, width: impl Into<Length>) -> Self {
199        self.width = width.into();
200        self
201    }
202
203    /// Height sizing intent.
204    #[must_use]
205    pub fn height(mut self, height: impl Into<Length>) -> Self {
206        self.height = height.into();
207        self
208    }
209
210    fn key_bounds_for(&self, bounds: Rectangle) -> Rectangle {
211        let reserve: u32 = if self.show_field { FIELD_H } else { 0 };
212        Rectangle::new(
213            Point::new(bounds.top_left.x, bounds.top_left.y + reserve as i32),
214            Size::new(
215                bounds.size.width,
216                bounds.size.height.saturating_sub(reserve),
217            ),
218        )
219    }
220
221    /// Rect of the password Show/Hide button inside the preview field, or
222    /// `None` when there is no field or it isn't a password keyboard.
223    fn show_button_rect(&self) -> Option<Rectangle> {
224        if !(self.is_password && self.show_field) {
225            return None;
226        }
227        let x0 = self.bounds.top_left.x;
228        let y0 = self.bounds.top_left.y;
229        let width = self.bounds.size.width;
230        Some(Rectangle::new(
231            Point::new(x0 + width as i32 - 8 - SHOW_W as i32, y0 + 30),
232            Size::new(SHOW_W, 28),
233        ))
234    }
235
236    fn build_keys<F>(mode: KeyboardMode, on_action: F) -> Column<'a, C, M>
237    where
238        F: Fn(KeyAction) -> M + Copy + 'a,
239    {
240        let mut col = Column::new().spacing(2);
241        for row in keymap(mode) {
242            col = col.push(Self::build_row(row, on_action));
243        }
244        col
245    }
246
247    fn build_row<F>(spec: Vec<Key>, on_action: F) -> Row<'a, C, M>
248    where
249        F: Fn(KeyAction) -> M + Copy + 'a,
250    {
251        let mut row = Row::new().spacing(2);
252        for key in spec {
253            let class = match key.action {
254                KeyAction::Ready => ButtonClass::Suggested,
255                KeyAction::Cancel => ButtonClass::Destructive,
256                _ => ButtonClass::Standard,
257            };
258            row = row.push(
259                Button::new(key.label)
260                    .on_press(on_action(key.action))
261                    .class(class)
262                    .width(Length::FillPortion(key.weight)),
263            );
264        }
265        row
266    }
267}
268
269/// The keymap (rows of keys) for a given mode, following LVGL's layouts.
270fn keymap(mode: KeyboardMode) -> Vec<Vec<Key>> {
271    use KeyAction::{Backspace, CursorLeft, CursorRight, Mode, Newline, Ready};
272    use KeyboardMode::{Number, Special, TextLower, TextUpper};
273
274    /// Helper: build a row from a leading control key, a run of chars, and a
275    /// trailing control key.
276    fn text_row(lead: Key, mids: &str, tail: Key) -> Vec<Key> {
277        let mut r = vec![lead];
278        r.extend(mids.chars().map(ch));
279        r.push(tail);
280        r
281    }
282
283    match mode {
284        TextLower => vec![
285            text_row(
286                ctrl("123", Mode(Special), 3),
287                "qwertyuiop",
288                ctrl("⌫", Backspace, 3),
289            ),
290            text_row(
291                ctrl("ABC", Mode(TextUpper), 3),
292                "asdfghjkl",
293                ctrl("↵", Newline, 3),
294            ),
295            "_-zxcvbnm.,:".chars().map(ch).collect(),
296            bottom_row(),
297        ],
298        TextUpper => vec![
299            text_row(
300                ctrl("123", Mode(Special), 3),
301                "QWERTYUIOP",
302                ctrl("⌫", Backspace, 3),
303            ),
304            text_row(
305                ctrl("abc", Mode(TextLower), 3),
306                "ASDFGHJKL",
307                ctrl("↵", Newline, 3),
308            ),
309            "_-ZXCVBNM.,:".chars().map(ch).collect(),
310            bottom_row(),
311        ],
312        Special => vec![
313            {
314                let mut r: Vec<Key> = "0123456789".chars().map(ch).collect();
315                r.push(ctrl("⌫", Backspace, 3));
316                r
317            },
318            text_row(
319                ctrl("abc", Mode(TextLower), 3),
320                "+-/*=%!?#<>",
321                ctrl("↵", Newline, 3),
322            ),
323            "\\@$(){}[];\"'".chars().map(ch).collect(),
324            bottom_row(),
325        ],
326        Number => vec![
327            {
328                let mut r: Vec<Key> = "123".chars().map(ch).collect();
329                r.push(ctrl("▼", KeyAction::Cancel, 1));
330                r
331            },
332            {
333                let mut r: Vec<Key> = "456".chars().map(ch).collect();
334                r.push(ctrl("OK", Ready, 1));
335                r
336            },
337            {
338                let mut r: Vec<Key> = "789".chars().map(ch).collect();
339                r.push(ctrl("⌫", Backspace, 1));
340                r
341            },
342            vec![
343                ctrl("abc", Mode(TextLower), 1),
344                ch('0'),
345                ch('.'),
346                ctrl("←", CursorLeft, 1),
347                ctrl("→", CursorRight, 1),
348            ],
349        ],
350    }
351}
352
353impl<'a, C: PixelColor + 'a, M: Clone + 'a> Widget<C, M> for Keyboard<'a, C, M> {
354    fn measure(&mut self, constraints: Constraints) -> Size {
355        let w = self
356            .width
357            .resolve(constraints.max.width, constraints.max.width);
358        let h = self
359            .height
360            .resolve(constraints.max.height, constraints.max.height);
361        constraints.clamp(Size::new(w, h))
362    }
363
364    fn preferred_size(&self) -> (Length, Length) {
365        (self.width, self.height)
366    }
367
368    fn arrange(&mut self, rect: Rectangle) {
369        self.bounds = rect;
370        let kb_bounds = self.key_bounds_for(rect);
371        self.keys.arrange(kb_bounds);
372    }
373
374    fn rect(&self) -> Rectangle {
375        self.bounds
376    }
377
378    fn handle_touch(&mut self, point: Point, phase: TouchPhase) -> Option<M> {
379        if let Some(btn) = self.show_button_rect() {
380            let br = btn.top_left + Point::new(btn.size.width as i32, btn.size.height as i32);
381            let inside = point.x >= btn.top_left.x
382                && point.x < br.x
383                && point.y >= btn.top_left.y
384                && point.y < br.y;
385            if inside {
386                // Toggle reveal on release; swallow press/move so the tap on
387                // the field strip never reaches the keys below.
388                return (phase == TouchPhase::Up).then(|| self.toggle_reveal.clone());
389            }
390        }
391        self.keys.handle_touch(point, phase)
392    }
393
394    fn mark_pressed(&mut self, point: Point) {
395        self.keys.mark_pressed(point);
396    }
397
398    fn draw<'t>(
399        &self,
400        renderer: &mut dyn Renderer<C>,
401        theme: &Theme<'t, C>,
402    ) -> Result<(), RenderError> {
403        renderer.fill_rect(self.bounds, theme.background.base)?;
404
405        if self.show_field {
406            let x0 = self.bounds.top_left.x;
407            let y0 = self.bounds.top_left.y;
408            let width = self.bounds.size.width;
409
410            // Title
411            renderer.draw_text(
412                &self.title,
413                Point::new(x0 + (width / 2) as i32, y0 + 18),
414                theme.default_font(),
415                theme.background.on_base,
416                Alignment::Center,
417            )?;
418
419            // Input field outline — narrowed to leave room for the password
420            // Show/Hide button when present.
421            let field_w = if self.is_password {
422                width.saturating_sub(16 + SHOW_W + 4)
423            } else {
424                width.saturating_sub(16)
425            };
426            let field = Rectangle::new(Point::new(x0 + 8, y0 + 30), Size::new(field_w, 28));
427            renderer.stroke_rect(field, theme.button.border)?;
428
429            // Masked unless the user tapped Show.
430            let display_text = if self.is_password && !self.reveal {
431                "*".repeat(self.input.chars().count())
432            } else {
433                self.input.clone()
434            };
435            renderer.draw_text(
436                &display_text,
437                Point::new(x0 + 14, y0 + 49),
438                theme.default_font(),
439                theme.background.on_base,
440                Alignment::Left,
441            )?;
442
443            // Password Show/Hide toggle.
444            if let Some(btn) = self.show_button_rect() {
445                renderer.fill_rect(btn, theme.button.base)?;
446                renderer.stroke_rect(btn, theme.button.border)?;
447                let label = if self.reveal { "Hide" } else { "Show" };
448                let glyph_h = theme.default_font().character_size.height as i32;
449                renderer.draw_text(
450                    label,
451                    Point::new(
452                        btn.top_left.x + btn.size.width as i32 / 2,
453                        btn.top_left.y + btn.size.height as i32 / 2 + glyph_h / 3,
454                    ),
455                    theme.default_font(),
456                    theme.button.on_base,
457                    Alignment::Center,
458                )?;
459            }
460        }
461
462        // Key grid.
463        self.keys.draw(renderer, theme)
464    }
465}