cadmus_core/view/
toggleable_keyboard.rs

1//! A reusable keyboard component that can be toggled on and off.
2//!
3//! `ToggleableKeyboard` encapsulates the keyboard view along with its separator,
4//! providing a clean API for managing keyboard visibility in parent views.
5
6use crate::color::BLACK;
7use crate::context::Context;
8use crate::device::CURRENT_DEVICE;
9use crate::font::Fonts;
10use crate::framebuffer::{Framebuffer, UpdateMode};
11use crate::geom::{halves, Rectangle};
12use crate::unit::scale_by_dpi;
13use crate::view::filler::Filler;
14use crate::view::keyboard::Keyboard;
15use crate::view::{Bus, Event, Hub, Id, RenderData, RenderQueue, View, ID_FEEDER};
16use crate::view::{BIG_BAR_HEIGHT, SMALL_BAR_HEIGHT, THICKNESS_MEDIUM};
17
18/// A view component that wraps a keyboard and provides toggle functionality.
19///
20/// This component manages a keyboard view along with a separator line,
21/// handling all the complexity of showing/hiding the keyboard, updating
22/// the context, and managing focus events.
23///
24/// # Examples
25///
26/// ```rust,ignore
27/// let keyboard = ToggleableKeyboard::new(parent_rect, false);
28/// children.push(Box::new(keyboard) as Box<dyn View>);
29///
30/// // Later, to toggle keyboard visibility:
31/// if let Some(index) = locate::<ToggleableKeyboard>(self) {
32///     let kb = self.children[index].downcast_mut::<ToggleableKeyboard>().unwrap();
33///     kb.toggle(hub, rq, context);  // Toggles between hidden/visible
34/// }
35/// ```
36pub struct ToggleableKeyboard {
37    id: Id,
38    rect: Rectangle,
39    children: Vec<Box<dyn View>>,
40    visible: bool,
41    parent_rect: Rectangle,
42    number_mode: bool,
43}
44
45impl ToggleableKeyboard {
46    /// Creates a new `ToggleableKeyboard` instance.
47    ///
48    /// The keyboard is initially hidden and must be explicitly shown
49    /// by calling `toggle(...)` or `set_visible(true, ...)`.
50    ///
51    /// # Arguments
52    ///
53    /// * `parent_rect` - The rectangle of the parent view, used for positioning
54    /// * `number_mode` - If `true`, the keyboard starts in number mode
55    ///
56    /// # Returns
57    ///
58    /// A new `ToggleableKeyboard` instance in hidden state.
59    pub fn new(parent_rect: Rectangle, number_mode: bool) -> Self {
60        ToggleableKeyboard {
61            id: ID_FEEDER.next(),
62            rect: Rectangle::default(),
63            children: Vec::new(),
64            visible: false,
65            parent_rect,
66            number_mode,
67        }
68    }
69
70    /// Toggles the keyboard visibility between hidden and visible.
71    ///
72    /// If the keyboard is currently hidden, it will be shown.
73    /// If the keyboard is currently visible, it will be hidden.
74    /// When hiding, this clears focus and updates the context.
75    ///
76    /// # Arguments
77    ///
78    /// * `hub` - Event hub for sending focus events
79    /// * `rq` - Render queue for scheduling redraws
80    /// * `context` - Application context for updating keyboard state
81    pub fn toggle(&mut self, hub: &Hub, rq: &mut RenderQueue, context: &mut Context) {
82        if self.visible {
83            self.hide(hub, rq, context);
84        } else {
85            self.show(rq, context);
86        }
87    }
88
89    /// Sets the keyboard visibility to the specified state.
90    ///
91    /// This is more explicit than `toggle()` when you know whether you want
92    /// to show or hide the keyboard. If the keyboard is already in the desired
93    /// state, this is a no-op.
94    ///
95    /// # Arguments
96    ///
97    /// * `visible` - `true` to show the keyboard, `false` to hide it
98    /// * `hub` - Event hub for sending focus events
99    /// * `rq` - Render queue for scheduling redraws
100    /// * `context` - Application context for updating keyboard state
101    pub fn set_visible(
102        &mut self,
103        visible: bool,
104        hub: &Hub,
105        rq: &mut RenderQueue,
106        context: &mut Context,
107    ) {
108        if self.visible == visible {
109            return;
110        }
111
112        if visible {
113            self.show(rq, context);
114        } else {
115            self.hide(hub, rq, context);
116        }
117    }
118
119    /// Sets the keyboard number mode.
120    ///
121    /// When number mode is enabled, the keyboard displays numbers and
122    /// symbols instead of letters. This setting only takes effect the
123    /// next time the keyboard is shown.
124    ///
125    /// # Arguments
126    ///
127    /// * `number_mode` - `true` to enable number mode, `false` for letter mode
128    pub fn set_number_mode(&mut self, number_mode: bool) {
129        self.number_mode = number_mode;
130    }
131
132    /// Returns whether the keyboard is currently visible.
133    ///
134    /// # Returns
135    ///
136    /// `true` if the keyboard is visible, `false` otherwise.
137    pub fn is_visible(&self) -> bool {
138        self.visible
139    }
140
141    /// Shows the keyboard by creating the separator and keyboard views.
142    ///
143    /// This method calculates the proper positioning based on the parent rect
144    /// and creates both the separator line and the keyboard itself.
145    fn show(&mut self, rq: &mut RenderQueue, context: &mut Context) {
146        let dpi = CURRENT_DEVICE.dpi;
147        let (small_height, big_height) = (
148            scale_by_dpi(SMALL_BAR_HEIGHT, dpi) as i32,
149            scale_by_dpi(BIG_BAR_HEIGHT, dpi) as i32,
150        );
151        let thickness = scale_by_dpi(THICKNESS_MEDIUM, dpi) as i32;
152        let (_small_thickness, big_thickness) = halves(thickness);
153
154        let separator = Filler::new(
155            rect![
156                self.parent_rect.min.x,
157                self.parent_rect.max.y - (small_height + 3 * big_height),
158                self.parent_rect.max.x,
159                self.parent_rect.max.y - (small_height + 3 * big_height) + thickness
160            ],
161            BLACK,
162        );
163        self.children.push(Box::new(separator) as Box<dyn View>);
164
165        let mut kb_rect = rect![
166            self.parent_rect.min.x,
167            self.parent_rect.max.y - (small_height + 3 * big_height) + big_thickness,
168            self.parent_rect.max.x,
169            self.parent_rect.max.y - small_height - big_thickness
170        ];
171
172        let keyboard = Keyboard::new(&mut kb_rect, self.number_mode, context);
173        self.children.push(Box::new(keyboard) as Box<dyn View>);
174
175        self.rect = kb_rect;
176        self.rect.absorb(self.children[0].rect());
177
178        self.visible = true;
179
180        rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui));
181    }
182
183    /// Hides the keyboard by clearing all child views and resetting state.
184    ///
185    /// This method also clears the focus and updates the context to reflect
186    /// that no keyboard is active.
187    fn hide(&mut self, hub: &Hub, rq: &mut RenderQueue, context: &mut Context) {
188        let rect = self.rect;
189
190        self.children.clear();
191
192        context.kb_rect = Rectangle::default();
193        self.rect = Rectangle::default();
194        self.visible = false;
195
196        hub.send(Event::Focus(None)).ok();
197        rq.add(RenderData::expose(rect, UpdateMode::Gui));
198    }
199}
200
201impl View for ToggleableKeyboard {
202    #[cfg_attr(feature = "otel", tracing::instrument(skip(self, hub, bus, rq, context), fields(event = ?evt), ret(level=tracing::Level::TRACE)))]
203    fn handle_event(
204        &mut self,
205        evt: &Event,
206        hub: &Hub,
207        bus: &mut Bus,
208        rq: &mut RenderQueue,
209        context: &mut Context,
210    ) -> bool {
211        if !self.visible {
212            return false;
213        }
214
215        for child in &mut self.children {
216            if child.handle_event(evt, hub, bus, rq, context) {
217                return true;
218            }
219        }
220
221        false
222    }
223    #[cfg_attr(feature = "otel", tracing::instrument(skip(self, fb, fonts), fields(rect = ?rect)))]
224    fn render(&self, fb: &mut dyn Framebuffer, rect: Rectangle, fonts: &mut Fonts) {
225        if !self.visible {
226            return;
227        }
228
229        for child in &self.children {
230            child.render(fb, rect, fonts);
231        }
232    }
233
234    fn rect(&self) -> &Rectangle {
235        &self.rect
236    }
237
238    fn rect_mut(&mut self) -> &mut Rectangle {
239        &mut self.rect
240    }
241
242    fn children(&self) -> &Vec<Box<dyn View>> {
243        &self.children
244    }
245
246    fn children_mut(&mut self) -> &mut Vec<Box<dyn View>> {
247        &mut self.children
248    }
249
250    fn id(&self) -> Id {
251        self.id
252    }
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258    use crate::battery::{Battery, FakeBattery};
259    use crate::framebuffer::Pixmap;
260    use crate::frontlight::{Frontlight, LightLevels};
261    use crate::library::Library;
262    use crate::lightsensor::LightSensor;
263    use crate::settings::{LibraryMode, Settings};
264    use std::env;
265    use std::path::Path;
266    use std::sync::mpsc::channel;
267
268    fn create_test_keyboard() -> ToggleableKeyboard {
269        let parent_rect = rect![0, 0, 600, 800];
270        ToggleableKeyboard::new(parent_rect, false)
271    }
272
273    fn create_test_context() -> Context {
274        let fb = Box::new(Pixmap::new(600, 800, 1)) as Box<dyn Framebuffer>;
275        let battery = Box::new(FakeBattery::new()) as Box<dyn Battery>;
276        let frontlight = Box::new(LightLevels::default()) as Box<dyn Frontlight>;
277        let lightsensor = Box::new(0u16) as Box<dyn LightSensor>;
278        let settings = Settings::default();
279        let library = Library::new(Path::new("."), LibraryMode::Database).unwrap_or_else(|_| {
280            Library::new(Path::new("/tmp"), LibraryMode::Database).expect(
281                "Failed to create test library. \
282                 Ensure /tmp directory exists and is writable.",
283            )
284        });
285        let fonts = Fonts::load_from(
286            Path::new(
287                &env::var("TEST_ROOT_DIR").expect("TEST_ROOT_DIR must be set for this test."),
288            )
289            .to_path_buf(),
290        )
291        .expect(
292            "Failed to load fonts. Tests require font files to be present. \
293             Run tests from the project root directory.",
294        );
295
296        let mut ctx = Context::new(
297            fb,
298            None,
299            library,
300            settings,
301            fonts,
302            battery,
303            frontlight,
304            lightsensor,
305        );
306        ctx.load_keyboard_layouts();
307        ctx.load_dictionaries();
308
309        ctx
310    }
311
312    #[test]
313    fn test_new_creates_hidden_keyboard() {
314        let keyboard = create_test_keyboard();
315        assert!(!keyboard.is_visible());
316        assert_eq!(keyboard.children.len(), 0);
317        assert!(!keyboard.number_mode);
318    }
319
320    #[test]
321    fn test_new_with_number_mode() {
322        let parent_rect = rect![0, 0, 600, 800];
323        let keyboard = ToggleableKeyboard::new(parent_rect, true);
324        assert!(keyboard.number_mode);
325    }
326
327    #[test]
328    fn test_is_visible_initially_false() {
329        let keyboard = create_test_keyboard();
330        assert!(!keyboard.is_visible());
331    }
332
333    #[test]
334    fn test_set_number_mode() {
335        let mut keyboard = create_test_keyboard();
336        assert!(!keyboard.number_mode);
337
338        keyboard.set_number_mode(true);
339        assert!(keyboard.number_mode);
340
341        keyboard.set_number_mode(false);
342        assert!(!keyboard.number_mode);
343    }
344
345    #[test]
346    fn test_rect_defaults_to_empty() {
347        let keyboard = create_test_keyboard();
348        let rect = keyboard.rect();
349        assert_eq!(rect.min.x, 0);
350        assert_eq!(rect.min.y, 0);
351        assert_eq!(rect.max.x, 0);
352        assert_eq!(rect.max.y, 0);
353    }
354
355    #[test]
356    fn test_children_empty_when_hidden() {
357        let keyboard = create_test_keyboard();
358        assert!(keyboard.children().is_empty());
359    }
360
361    #[test]
362    fn test_parent_rect_stored_correctly() {
363        let parent_rect = rect![10, 20, 590, 780];
364        let keyboard = ToggleableKeyboard::new(parent_rect, false);
365        assert_eq!(keyboard.parent_rect, parent_rect);
366    }
367
368    #[test]
369    fn test_toggle_from_hidden_shows_keyboard() {
370        let mut keyboard = create_test_keyboard();
371        let (hub, _receiver) = channel();
372        let mut rq = RenderQueue::new();
373        let mut context = create_test_context();
374
375        assert!(!keyboard.is_visible());
376        assert!(keyboard.children.is_empty());
377        assert!(rq.is_empty());
378
379        keyboard.toggle(&hub, &mut rq, &mut context);
380
381        assert!(keyboard.is_visible());
382        assert_eq!(keyboard.children.len(), 2);
383        assert_eq!(rq.len(), 1);
384    }
385
386    #[test]
387    fn test_toggle_from_visible_hides_keyboard() {
388        let mut keyboard = create_test_keyboard();
389        let (hub, receiver) = channel();
390        let mut rq = RenderQueue::new();
391        let mut context = create_test_context();
392
393        keyboard.toggle(&hub, &mut rq, &mut context);
394        assert!(keyboard.is_visible());
395        assert_eq!(keyboard.children.len(), 2);
396
397        rq = RenderQueue::new();
398        keyboard.toggle(&hub, &mut rq, &mut context);
399
400        assert!(!keyboard.is_visible());
401        assert!(keyboard.children.is_empty());
402        assert_eq!(rq.len(), 1);
403        assert_eq!(context.kb_rect, Rectangle::default());
404
405        let focus_event = receiver.try_recv().unwrap();
406        assert!(matches!(focus_event, Event::Focus(None)));
407    }
408
409    #[test]
410    fn test_toggle_twice_returns_to_original_state() {
411        let mut keyboard = create_test_keyboard();
412        let (hub, _receiver) = channel();
413        let mut rq = RenderQueue::new();
414        let mut context = create_test_context();
415
416        keyboard.toggle(&hub, &mut rq, &mut context);
417        keyboard.toggle(&hub, &mut rq, &mut context);
418
419        assert!(!keyboard.is_visible());
420        assert!(keyboard.children.is_empty());
421    }
422
423    #[test]
424    fn test_toggle_adds_render_data_each_time() {
425        let mut keyboard = create_test_keyboard();
426        let (hub, _receiver) = channel();
427        let mut context = create_test_context();
428
429        let mut rq = RenderQueue::new();
430        keyboard.toggle(&hub, &mut rq, &mut context);
431        assert_eq!(rq.len(), 1);
432
433        let mut rq = RenderQueue::new();
434        keyboard.toggle(&hub, &mut rq, &mut context);
435        assert_eq!(rq.len(), 1);
436    }
437
438    #[test]
439    fn test_set_visible_true_shows_keyboard() {
440        let mut keyboard = create_test_keyboard();
441        let (hub, _receiver) = channel();
442        let mut rq = RenderQueue::new();
443        let mut context = create_test_context();
444
445        assert!(!keyboard.is_visible());
446        assert!(keyboard.children.is_empty());
447
448        keyboard.set_visible(true, &hub, &mut rq, &mut context);
449
450        assert!(keyboard.is_visible());
451        assert_eq!(keyboard.children.len(), 2);
452        assert_eq!(rq.len(), 1);
453    }
454
455    #[test]
456    fn test_set_visible_false_hides_keyboard() {
457        let mut keyboard = create_test_keyboard();
458        let (hub, receiver) = channel();
459        let mut rq = RenderQueue::new();
460        let mut context = create_test_context();
461
462        keyboard.set_visible(true, &hub, &mut rq, &mut context);
463        assert!(keyboard.is_visible());
464        assert_eq!(keyboard.children.len(), 2);
465
466        rq = RenderQueue::new();
467        keyboard.set_visible(false, &hub, &mut rq, &mut context);
468
469        assert!(!keyboard.is_visible());
470        assert!(keyboard.children.is_empty());
471        assert_eq!(rq.len(), 1);
472        assert_eq!(context.kb_rect, Rectangle::default());
473
474        let focus_event = receiver.try_recv().unwrap();
475        assert!(matches!(focus_event, Event::Focus(None)));
476    }
477
478    #[test]
479    fn test_set_visible_noop_when_already_visible() {
480        let mut keyboard = create_test_keyboard();
481        let (hub, _receiver) = channel();
482        let mut rq = RenderQueue::new();
483        let mut context = create_test_context();
484
485        keyboard.set_visible(true, &hub, &mut rq, &mut context);
486        assert!(keyboard.is_visible());
487
488        rq = RenderQueue::new();
489        keyboard.set_visible(true, &hub, &mut rq, &mut context);
490
491        assert!(keyboard.is_visible());
492        assert!(rq.is_empty());
493    }
494
495    #[test]
496    fn test_set_visible_noop_when_already_hidden() {
497        let mut keyboard = create_test_keyboard();
498        let (hub, _receiver) = channel();
499        let mut rq = RenderQueue::new();
500        let mut context = create_test_context();
501
502        assert!(!keyboard.is_visible());
503
504        keyboard.set_visible(false, &hub, &mut rq, &mut context);
505
506        assert!(!keyboard.is_visible());
507        assert!(rq.is_empty());
508    }
509}