Skip to main content

rlvgl_widgets/
keyboard.rs

1//! Specialized [`ButtonMatrix`] with predefined key maps and key-output hook
2//! (LPAR-13 §5.D).
3//!
4//! A [`Keyboard`] owns a [`ButtonMatrix`] internally and switches between
5//! predefined button maps via [`set_mode`](Keyboard::set_mode).  Pressing a
6//! key resolves to a [`KeyOutput`] value delivered to the caller via:
7//!
8//! 1. **Hook** — install a `FnMut(KeyOutput) + 'static` closure with
9//!    [`set_key_output_hook`](Keyboard::set_key_output_hook).
10//! 2. **Poll slot** — call [`last_key_output`](Keyboard::last_key_output) once
11//!    per frame; it drains and returns the most-recently-produced output.
12//!
13//! Before LPAR-14 Textarea v2 the app wires the hook to any text field
14//! manually (e.g. `textarea.insert_char(ch)`).
15//!
16//! # Key navigation (LPAR-12/LPAR-13 §5.J)
17//!
18//! Wire `ObjectEvent::Key` to the imperative helpers:
19//!
20//! ```ignore
21//! keyboard.navigate_next();        // ArrowRight / Tab
22//! keyboard.navigate_prev();        // ArrowLeft  / BackTab
23//! keyboard.activate_selected();    // Enter
24//! ```
25
26use alloc::boxed::Box;
27use alloc::string::String;
28use alloc::string::ToString;
29use rlvgl_core::event::Event;
30use rlvgl_core::renderer::Renderer;
31use rlvgl_core::widget::{Rect, Widget};
32
33use crate::button_matrix::{BUTTON_NONE, ButtonId, ButtonMatrix};
34
35// ── Predefined key maps ───────────────────────────────────────────────────────
36
37/// Lowercase alphabetic map (mirrors `lv_kb_def_btnm_map_lc`).
38static MAP_LOWER: &[&str] = &[
39    "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "\n", "a", "s", "d", "f", "g", "h", "j", "k",
40    "l", "\n", "ABC", "z", "x", "c", "v", "b", "n", "m", "⌫", "\n", "123", " ", "↵",
41];
42
43/// Uppercase alphabetic map (mirrors `lv_kb_def_btnm_map_uc`).
44static MAP_UPPER: &[&str] = &[
45    "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "\n", "A", "S", "D", "F", "G", "H", "J", "K",
46    "L", "\n", "abc", "Z", "X", "C", "V", "B", "N", "M", "⌫", "\n", "123", " ", "↵",
47];
48
49/// Numeric / symbol map (mirrors `lv_kb_def_btnm_map_num`).
50static MAP_NUMBER: &[&str] = &[
51    "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "\n", "+", "-", "/", "*", "=", "%", "!", "?",
52    "#", "<", ">", "\n", "\\", "@", "$", "(", ")", "{", "}", "[", "]", ";", ":", "'", "\n", "abc",
53    " ", "↵", "⌫",
54];
55
56/// Special / emoji placeholder map.
57static MAP_SPECIAL: &[&str] = &[
58    ":)", ":(", ";)", ":D", ":O", ":-|", ":P", "\n", "abc", " ", "↵", "⌫",
59];
60
61// ── Public types ──────────────────────────────────────────────────────────────
62
63/// Named key-map configuration for [`Keyboard`].
64///
65/// Registration policy: Specification Required (LPAR-13 §7).
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum KeyboardMode {
68    /// Lowercase alphabetic layout.
69    TextLower,
70    /// Uppercase alphabetic layout.
71    TextUpper,
72    /// Numeric and symbol layout.
73    Number,
74    /// Special characters / emoji placeholder layout.
75    Special,
76    /// User-defined slot 1.
77    User1,
78    /// User-defined slot 2.
79    User2,
80    /// User-defined slot 3.
81    User3,
82    /// User-defined slot 4.
83    User4,
84}
85
86/// Internal control action produced by a special key in the keyboard map.
87///
88/// Registration policy: Specification Required (LPAR-13 §7).
89#[derive(Debug, Clone, PartialEq, Eq)]
90pub enum KeyboardControl {
91    /// Switch to the indicated keyboard mode.
92    SwitchMode(KeyboardMode),
93}
94
95/// Value produced by a key activation on a [`Keyboard`].
96///
97/// Registration policy: Specification Required (LPAR-13 §7).
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub enum KeyOutput {
100    /// A printable character was pressed.
101    Char(char),
102    /// The backspace key was pressed (delete character before cursor).
103    Backspace,
104    /// The enter / return key was pressed.
105    Enter,
106    /// The tab key was pressed.
107    Tab,
108    /// The escape key was pressed.
109    Escape,
110    /// An internal control action (mode switch, etc.).
111    Control(KeyboardControl),
112}
113
114/// Resolve a button label string from a predefined map to a [`KeyOutput`].
115fn resolve_key(label: &str) -> Option<KeyOutput> {
116    match label {
117        "⌫" => Some(KeyOutput::Backspace),
118        "↵" => Some(KeyOutput::Enter),
119        "ABC" => Some(KeyOutput::Control(KeyboardControl::SwitchMode(
120            KeyboardMode::TextUpper,
121        ))),
122        "abc" => Some(KeyOutput::Control(KeyboardControl::SwitchMode(
123            KeyboardMode::TextLower,
124        ))),
125        "123" => Some(KeyOutput::Control(KeyboardControl::SwitchMode(
126            KeyboardMode::Number,
127        ))),
128        " " => Some(KeyOutput::Char(' ')),
129        s if s.chars().count() == 1 => Some(KeyOutput::Char(s.chars().next().unwrap())),
130        _ => None,
131    }
132}
133
134// ── Keyboard ──────────────────────────────────────────────────────────────────
135
136/// Specialized button-grid keyboard with predefined mode maps (LPAR-13 §5.D).
137///
138/// Owns a [`ButtonMatrix`] internally.  Mode switching replaces the inner map.
139/// Key output is delivered via the hook closure and/or the poll slot.
140pub struct Keyboard {
141    matrix: ButtonMatrix,
142    mode: KeyboardMode,
143    popovers: bool,
144    /// Most-recently produced key output (poll slot).
145    last_output: Option<KeyOutput>,
146    /// Optional callback invoked on each key activation.
147    hook: Option<Box<dyn FnMut(KeyOutput)>>,
148}
149
150impl Keyboard {
151    /// Create a [`Keyboard`] occupying `bounds` in [`KeyboardMode::TextLower`].
152    pub fn new(bounds: Rect) -> Self {
153        let mut matrix = ButtonMatrix::new(bounds);
154        matrix.set_map(MAP_LOWER);
155        Self {
156            matrix,
157            mode: KeyboardMode::TextLower,
158            popovers: false,
159            last_output: None,
160            hook: None,
161        }
162    }
163
164    // ── Mode ──────────────────────────────────────────────────────────────
165
166    /// Set the active key map mode and update the inner [`ButtonMatrix`] map.
167    pub fn set_mode(&mut self, mode: KeyboardMode) {
168        self.mode = mode;
169        let map: &[&str] = match mode {
170            KeyboardMode::TextLower => MAP_LOWER,
171            KeyboardMode::TextUpper => MAP_UPPER,
172            KeyboardMode::Number => MAP_NUMBER,
173            KeyboardMode::Special => MAP_SPECIAL,
174            // User-defined: no built-in map; leave current matrix unchanged.
175            KeyboardMode::User1
176            | KeyboardMode::User2
177            | KeyboardMode::User3
178            | KeyboardMode::User4 => return,
179        };
180        self.matrix.set_map(map);
181    }
182
183    /// Return the currently active mode.
184    pub fn mode(&self) -> KeyboardMode {
185        self.mode
186    }
187
188    // ── Popovers ──────────────────────────────────────────────────────────
189
190    /// Store the popover preference (forwarded to the inner matrix; v1 drawing
191    /// behavior may be a no-op pending LPAR-12 §13 deferred-Safe).
192    pub fn set_popovers(&mut self, enable: bool) {
193        self.popovers = enable;
194    }
195
196    /// Return whether popovers are requested.
197    pub fn popovers(&self) -> bool {
198        self.popovers
199    }
200
201    // ── Key output hook ───────────────────────────────────────────────────
202
203    /// Install a callback invoked on every key activation.
204    ///
205    /// The hook is stored as `Box<dyn FnMut(KeyOutput)>` so it is independent
206    /// of any specific text-field lifetime (LPAR-13 §5.D, `no_std + alloc`).
207    pub fn set_key_output_hook<F>(&mut self, hook: F)
208    where
209        F: FnMut(KeyOutput) + 'static,
210    {
211        self.hook = Some(Box::new(hook));
212    }
213
214    /// Drain and return the most-recently produced [`KeyOutput`] since the last
215    /// call.  Returns `None` if no key was pressed since the last poll.
216    pub fn last_key_output(&mut self) -> Option<KeyOutput> {
217        self.last_output.take()
218    }
219
220    // ── Key navigation helpers (LPAR-13 §5.J) ────────────────────────────
221
222    /// Select the next navigable button (delegates to inner [`ButtonMatrix`]).
223    ///
224    /// Wire to `ObjectEvent::Key(Key::ArrowRight)` or `Key::Tab`.
225    pub fn navigate_next(&mut self) {
226        self.matrix.navigate_next();
227    }
228
229    /// Select the previous navigable button (delegates to inner [`ButtonMatrix`]).
230    ///
231    /// Wire to `ObjectEvent::Key(Key::ArrowLeft)` or `Key::BackTab`.
232    pub fn navigate_prev(&mut self) {
233        self.matrix.navigate_prev();
234    }
235
236    /// Activate the currently selected button (delegates to inner
237    /// [`ButtonMatrix`]) and emit any resulting [`KeyOutput`].
238    ///
239    /// Wire to `ObjectEvent::Key(Key::Enter)`.
240    pub fn activate_selected(&mut self) {
241        let id = self.matrix.selected_button();
242        if id == BUTTON_NONE {
243            return;
244        }
245        // Snapshot label before mutable call
246        let label_owned: String = self.matrix.button_text(id).unwrap_or_default().to_string();
247        self.matrix.activate_selected();
248        self.emit_for_label(&label_owned);
249    }
250
251    // ── Internal ──────────────────────────────────────────────────────────
252
253    /// Emit a [`KeyOutput`] for the given label string, handling mode-switch
254    /// side-effects inline.
255    fn emit_for_label(&mut self, label: &str) {
256        let output = match resolve_key(label) {
257            Some(out) => out,
258            None => return,
259        };
260
261        // Apply mode-switch side-effect before delivering to caller
262        if let KeyOutput::Control(KeyboardControl::SwitchMode(new_mode)) = &output {
263            let m = *new_mode;
264            self.set_mode(m);
265        }
266
267        // Deliver via poll slot
268        self.last_output = Some(output.clone());
269
270        // Deliver via hook (takes ownership of output)
271        if let Some(hook) = &mut self.hook {
272            hook(output);
273        }
274    }
275
276    /// Return a reference to the inner [`ButtonMatrix`] for style customization.
277    pub fn matrix(&self) -> &ButtonMatrix {
278        &self.matrix
279    }
280
281    /// Return a mutable reference to the inner [`ButtonMatrix`].
282    pub fn matrix_mut(&mut self) -> &mut ButtonMatrix {
283        &mut self.matrix
284    }
285
286    /// Return the currently selected button id in the inner matrix.
287    pub fn selected_button(&self) -> ButtonId {
288        self.matrix.selected_button()
289    }
290}
291
292impl Widget for Keyboard {
293    fn bounds(&self) -> Rect {
294        self.matrix.bounds()
295    }
296
297    fn set_bounds(&mut self, bounds: Rect) {
298        self.matrix.set_bounds(bounds);
299    }
300
301    fn draw(&self, renderer: &mut dyn Renderer) {
302        self.matrix.draw(renderer);
303    }
304
305    fn handle_event(&mut self, event: &Event) -> bool {
306        let consumed = self.matrix.handle_event(event);
307        if consumed && matches!(event, Event::PressRelease { .. }) {
308            // The ButtonMatrix activated a button on PressRelease.
309            // Emit the corresponding KeyOutput for the now-selected button.
310            let activated_id = self.matrix.selected_button();
311            if activated_id != BUTTON_NONE {
312                let label_owned: String = self
313                    .matrix
314                    .button_text(activated_id)
315                    .unwrap_or_default()
316                    .to_string();
317                self.emit_for_label(&label_owned);
318            }
319        }
320        consumed
321    }
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327    use rlvgl_core::widget::{Color, Rect};
328
329    fn rect(x: i32, y: i32, w: i32, h: i32) -> Rect {
330        Rect {
331            x,
332            y,
333            width: w,
334            height: h,
335        }
336    }
337
338    struct NullRenderer;
339    impl rlvgl_core::renderer::Renderer for NullRenderer {
340        fn fill_rect(&mut self, _: Rect, _: Color) {}
341        fn draw_text(&mut self, _: (i32, i32), _: &str, _: Color) {}
342    }
343
344    #[test]
345    fn new_starts_in_lower_mode() {
346        let kb = Keyboard::new(rect(0, 0, 320, 160));
347        assert_eq!(kb.mode(), KeyboardMode::TextLower);
348    }
349
350    #[test]
351    fn set_mode_upper_changes_map() {
352        let mut kb = Keyboard::new(rect(0, 0, 320, 160));
353        kb.set_mode(KeyboardMode::TextUpper);
354        assert_eq!(kb.mode(), KeyboardMode::TextUpper);
355        // First button in upper map should be 'Q'
356        assert_eq!(kb.matrix().button_text(ButtonId(0)), Some("Q"));
357    }
358
359    #[test]
360    fn set_mode_number_changes_map() {
361        let mut kb = Keyboard::new(rect(0, 0, 320, 160));
362        kb.set_mode(KeyboardMode::Number);
363        assert_eq!(kb.mode(), KeyboardMode::Number);
364        assert_eq!(kb.matrix().button_text(ButtonId(0)), Some("1"));
365    }
366
367    #[test]
368    fn set_mode_special_changes_map() {
369        let mut kb = Keyboard::new(rect(0, 0, 320, 160));
370        kb.set_mode(KeyboardMode::Special);
371        assert_eq!(kb.mode(), KeyboardMode::Special);
372    }
373
374    #[test]
375    fn activate_char_key_emits_char_output() {
376        let mut kb = Keyboard::new(rect(0, 0, 320, 160));
377        // Navigate to the 'q' key (button 0 in lower mode)
378        kb.matrix_mut().set_selected_button(ButtonId(0));
379        kb.activate_selected();
380        assert_eq!(kb.last_key_output(), Some(KeyOutput::Char('q')));
381        // Poll slot is drained
382        assert_eq!(kb.last_key_output(), None);
383    }
384
385    #[test]
386    fn activate_backspace_emits_backspace() {
387        let mut kb = Keyboard::new(rect(0, 0, 320, 160));
388        // Find backspace button id by scanning
389        let count = kb.matrix().button_count();
390        let mut bs_id = None;
391        for i in 0..count {
392            if kb.matrix().button_text(ButtonId(i as u16)) == Some("⌫") {
393                bs_id = Some(ButtonId(i as u16));
394                break;
395            }
396        }
397        let bs_id = bs_id.expect("backspace button not found");
398        kb.matrix_mut().set_selected_button(bs_id);
399        kb.activate_selected();
400        assert_eq!(kb.last_key_output(), Some(KeyOutput::Backspace));
401    }
402
403    #[test]
404    fn activate_enter_emits_enter() {
405        let mut kb = Keyboard::new(rect(0, 0, 320, 160));
406        let count = kb.matrix().button_count();
407        let mut enter_id = None;
408        for i in 0..count {
409            if kb.matrix().button_text(ButtonId(i as u16)) == Some("↵") {
410                enter_id = Some(ButtonId(i as u16));
411                break;
412            }
413        }
414        let enter_id = enter_id.expect("enter button not found");
415        kb.matrix_mut().set_selected_button(enter_id);
416        kb.activate_selected();
417        assert_eq!(kb.last_key_output(), Some(KeyOutput::Enter));
418    }
419
420    #[test]
421    fn mode_switch_key_changes_mode() {
422        let mut kb = Keyboard::new(rect(0, 0, 320, 160));
423        // Find the ABC (switch to upper) button in lower mode
424        let count = kb.matrix().button_count();
425        let mut abc_id = None;
426        for i in 0..count {
427            if kb.matrix().button_text(ButtonId(i as u16)) == Some("ABC") {
428                abc_id = Some(ButtonId(i as u16));
429                break;
430            }
431        }
432        let abc_id = abc_id.expect("ABC mode-switch button not found");
433        kb.matrix_mut().set_selected_button(abc_id);
434        kb.activate_selected();
435        assert_eq!(kb.mode(), KeyboardMode::TextUpper);
436    }
437
438    #[test]
439    fn hook_is_called_on_key_activation() {
440        use alloc::rc::Rc;
441        use core::cell::Cell;
442
443        let called = Rc::new(Cell::new(false));
444        let called2 = called.clone();
445
446        let mut kb = Keyboard::new(rect(0, 0, 320, 160));
447        kb.set_key_output_hook(move |_out| {
448            called2.set(true);
449        });
450        kb.matrix_mut().set_selected_button(ButtonId(0));
451        kb.activate_selected();
452        assert!(called.get());
453    }
454
455    #[test]
456    fn navigate_next_and_prev_work() {
457        let mut kb = Keyboard::new(rect(0, 0, 320, 160));
458        kb.navigate_next();
459        assert_eq!(kb.selected_button(), ButtonId(0));
460        kb.navigate_next();
461        assert_eq!(kb.selected_button(), ButtonId(1));
462        kb.navigate_prev();
463        assert_eq!(kb.selected_button(), ButtonId(0));
464    }
465
466    #[test]
467    fn set_bounds_propagates_to_matrix() {
468        let mut kb = Keyboard::new(rect(0, 0, 320, 160));
469        kb.set_bounds(rect(10, 20, 400, 200));
470        assert_eq!(kb.bounds(), rect(10, 20, 400, 200));
471    }
472
473    #[test]
474    fn draw_does_not_panic() {
475        let kb = Keyboard::new(rect(0, 0, 320, 160));
476        let mut r = NullRenderer;
477        kb.draw(&mut r);
478    }
479
480    #[test]
481    fn popovers_stored() {
482        let mut kb = Keyboard::new(rect(0, 0, 320, 160));
483        kb.set_popovers(true);
484        assert!(kb.popovers());
485        kb.set_popovers(false);
486        assert!(!kb.popovers());
487    }
488}