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::context::test_helpers::create_test_context;
259    use std::sync::mpsc::channel;
260
261    fn create_test_keyboard() -> ToggleableKeyboard {
262        let parent_rect = rect![0, 0, 600, 800];
263        ToggleableKeyboard::new(parent_rect, false)
264    }
265
266    fn create_test_context_with_keyboard_data() -> Context {
267        let mut context = create_test_context();
268        context.load_keyboard_layouts();
269        context.load_dictionaries();
270        context
271    }
272
273    #[test]
274    fn test_new_creates_hidden_keyboard() {
275        let keyboard = create_test_keyboard();
276        assert!(!keyboard.is_visible());
277        assert_eq!(keyboard.children.len(), 0);
278        assert!(!keyboard.number_mode);
279    }
280
281    #[test]
282    fn test_new_with_number_mode() {
283        let parent_rect = rect![0, 0, 600, 800];
284        let keyboard = ToggleableKeyboard::new(parent_rect, true);
285        assert!(keyboard.number_mode);
286    }
287
288    #[test]
289    fn test_is_visible_initially_false() {
290        let keyboard = create_test_keyboard();
291        assert!(!keyboard.is_visible());
292    }
293
294    #[test]
295    fn test_set_number_mode() {
296        let mut keyboard = create_test_keyboard();
297        assert!(!keyboard.number_mode);
298
299        keyboard.set_number_mode(true);
300        assert!(keyboard.number_mode);
301
302        keyboard.set_number_mode(false);
303        assert!(!keyboard.number_mode);
304    }
305
306    #[test]
307    fn test_rect_defaults_to_empty() {
308        let keyboard = create_test_keyboard();
309        let rect = keyboard.rect();
310        assert_eq!(rect.min.x, 0);
311        assert_eq!(rect.min.y, 0);
312        assert_eq!(rect.max.x, 0);
313        assert_eq!(rect.max.y, 0);
314    }
315
316    #[test]
317    fn test_children_empty_when_hidden() {
318        let keyboard = create_test_keyboard();
319        assert!(keyboard.children().is_empty());
320    }
321
322    #[test]
323    fn test_parent_rect_stored_correctly() {
324        let parent_rect = rect![10, 20, 590, 780];
325        let keyboard = ToggleableKeyboard::new(parent_rect, false);
326        assert_eq!(keyboard.parent_rect, parent_rect);
327    }
328
329    #[test]
330    fn test_toggle_from_hidden_shows_keyboard() {
331        let mut keyboard = create_test_keyboard();
332        let (hub, _receiver) = channel();
333        let mut rq = RenderQueue::new();
334        let mut context = create_test_context_with_keyboard_data();
335
336        assert!(!keyboard.is_visible());
337        assert!(keyboard.children.is_empty());
338        assert!(rq.is_empty());
339
340        keyboard.toggle(&hub, &mut rq, &mut context);
341
342        assert!(keyboard.is_visible());
343        assert_eq!(keyboard.children.len(), 2);
344        assert_eq!(rq.len(), 1);
345    }
346
347    #[test]
348    fn test_toggle_from_visible_hides_keyboard() {
349        let mut keyboard = create_test_keyboard();
350        let (hub, receiver) = channel();
351        let mut rq = RenderQueue::new();
352        let mut context = create_test_context_with_keyboard_data();
353
354        keyboard.toggle(&hub, &mut rq, &mut context);
355        assert!(keyboard.is_visible());
356        assert_eq!(keyboard.children.len(), 2);
357
358        rq = RenderQueue::new();
359        keyboard.toggle(&hub, &mut rq, &mut context);
360
361        assert!(!keyboard.is_visible());
362        assert!(keyboard.children.is_empty());
363        assert_eq!(rq.len(), 1);
364        assert_eq!(context.kb_rect, Rectangle::default());
365
366        let focus_event = receiver.try_recv().unwrap();
367        assert!(matches!(focus_event, Event::Focus(None)));
368    }
369
370    #[test]
371    fn test_toggle_twice_returns_to_original_state() {
372        let mut keyboard = create_test_keyboard();
373        let (hub, _receiver) = channel();
374        let mut rq = RenderQueue::new();
375        let mut context = create_test_context_with_keyboard_data();
376
377        keyboard.toggle(&hub, &mut rq, &mut context);
378        keyboard.toggle(&hub, &mut rq, &mut context);
379
380        assert!(!keyboard.is_visible());
381        assert!(keyboard.children.is_empty());
382    }
383
384    #[test]
385    fn test_toggle_adds_render_data_each_time() {
386        let mut keyboard = create_test_keyboard();
387        let (hub, _receiver) = channel();
388        let mut context = create_test_context_with_keyboard_data();
389
390        let mut rq = RenderQueue::new();
391        keyboard.toggle(&hub, &mut rq, &mut context);
392        assert_eq!(rq.len(), 1);
393
394        let mut rq = RenderQueue::new();
395        keyboard.toggle(&hub, &mut rq, &mut context);
396        assert_eq!(rq.len(), 1);
397    }
398
399    #[test]
400    fn test_set_visible_true_shows_keyboard() {
401        let mut keyboard = create_test_keyboard();
402        let (hub, _receiver) = channel();
403        let mut rq = RenderQueue::new();
404        let mut context = create_test_context_with_keyboard_data();
405
406        assert!(!keyboard.is_visible());
407        assert!(keyboard.children.is_empty());
408
409        keyboard.set_visible(true, &hub, &mut rq, &mut context);
410
411        assert!(keyboard.is_visible());
412        assert_eq!(keyboard.children.len(), 2);
413        assert_eq!(rq.len(), 1);
414    }
415
416    #[test]
417    fn test_set_visible_false_hides_keyboard() {
418        let mut keyboard = create_test_keyboard();
419        let (hub, receiver) = channel();
420        let mut rq = RenderQueue::new();
421        let mut context = create_test_context_with_keyboard_data();
422
423        keyboard.set_visible(true, &hub, &mut rq, &mut context);
424        assert!(keyboard.is_visible());
425        assert_eq!(keyboard.children.len(), 2);
426
427        rq = RenderQueue::new();
428        keyboard.set_visible(false, &hub, &mut rq, &mut context);
429
430        assert!(!keyboard.is_visible());
431        assert!(keyboard.children.is_empty());
432        assert_eq!(rq.len(), 1);
433        assert_eq!(context.kb_rect, Rectangle::default());
434
435        let focus_event = receiver.try_recv().unwrap();
436        assert!(matches!(focus_event, Event::Focus(None)));
437    }
438
439    #[test]
440    fn test_set_visible_noop_when_already_visible() {
441        let mut keyboard = create_test_keyboard();
442        let (hub, _receiver) = channel();
443        let mut rq = RenderQueue::new();
444        let mut context = create_test_context_with_keyboard_data();
445
446        keyboard.set_visible(true, &hub, &mut rq, &mut context);
447        assert!(keyboard.is_visible());
448
449        rq = RenderQueue::new();
450        keyboard.set_visible(true, &hub, &mut rq, &mut context);
451
452        assert!(keyboard.is_visible());
453        assert!(rq.is_empty());
454    }
455
456    #[test]
457    fn test_set_visible_noop_when_already_hidden() {
458        let mut keyboard = create_test_keyboard();
459        let (hub, _receiver) = channel();
460        let mut rq = RenderQueue::new();
461        let mut context = create_test_context_with_keyboard_data();
462
463        assert!(!keyboard.is_visible());
464
465        keyboard.set_visible(false, &hub, &mut rq, &mut context);
466
467        assert!(!keyboard.is_visible());
468        assert!(rq.is_empty());
469    }
470}