Skip to main content

aprender_test_showcase/tui/
keypad.rs

1//! Numerical Keypad for TUI Calculator
2//!
3//! Probar: Visual feedback - Visual buttons make calculator state obvious
4//!
5//! This module provides an interactive numerical keypad that can be:
6//! - Clicked with mouse (TUI mouse events)
7//! - Highlighted when corresponding key is pressed
8//! - Used for visual demonstration of calculator operation
9
10use ratatui::{
11    buffer::Buffer,
12    layout::Rect,
13    style::{Color, Modifier, Style},
14    text::Span,
15    widgets::{Block, Borders, Widget},
16};
17
18/// A single keypad button
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct KeypadButton {
21    /// The character/symbol on the button
22    pub label: char,
23    /// Whether the button is currently pressed/highlighted
24    pub pressed: bool,
25    /// The action this button performs
26    pub action: ButtonAction,
27}
28
29/// Actions that keypad buttons can perform
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum ButtonAction {
32    /// Insert a digit (0-9)
33    Digit(u8),
34    /// Insert a decimal point
35    Decimal,
36    /// Insert an operator
37    Operator(char),
38    /// Evaluate the expression
39    Equals,
40    /// Clear the input
41    Clear,
42    /// Open parenthesis
43    OpenParen,
44    /// Close parenthesis
45    CloseParen,
46}
47
48impl KeypadButton {
49    /// Creates a new digit button
50    #[must_use]
51    pub fn digit(d: u8) -> Self {
52        Self {
53            label: char::from_digit(d as u32, 10).unwrap_or('?'),
54            pressed: false,
55            action: ButtonAction::Digit(d),
56        }
57    }
58
59    /// Creates a new operator button
60    #[must_use]
61    pub fn operator(op: char) -> Self {
62        Self {
63            label: op,
64            pressed: false,
65            action: ButtonAction::Operator(op),
66        }
67    }
68
69    /// Creates the decimal point button
70    #[must_use]
71    pub fn decimal() -> Self {
72        Self {
73            label: '.',
74            pressed: false,
75            action: ButtonAction::Decimal,
76        }
77    }
78
79    /// Creates the equals button
80    #[must_use]
81    pub fn equals() -> Self {
82        Self {
83            label: '=',
84            pressed: false,
85            action: ButtonAction::Equals,
86        }
87    }
88
89    /// Creates the clear button
90    #[must_use]
91    pub fn clear() -> Self {
92        Self {
93            label: 'C',
94            pressed: false,
95            action: ButtonAction::Clear,
96        }
97    }
98
99    /// Creates the open parenthesis button
100    #[must_use]
101    pub fn open_paren() -> Self {
102        Self {
103            label: '(',
104            pressed: false,
105            action: ButtonAction::OpenParen,
106        }
107    }
108
109    /// Creates the close parenthesis button
110    #[must_use]
111    pub fn close_paren() -> Self {
112        Self {
113            label: ')',
114            pressed: false,
115            action: ButtonAction::CloseParen,
116        }
117    }
118
119    /// Sets the pressed state
120    pub fn set_pressed(&mut self, pressed: bool) {
121        self.pressed = pressed;
122    }
123
124    /// Returns the character to insert for this button
125    #[must_use]
126    pub fn to_char(&self) -> Option<char> {
127        match self.action {
128            ButtonAction::Digit(d) => char::from_digit(d as u32, 10),
129            ButtonAction::Decimal => Some('.'),
130            ButtonAction::Operator(op) => Some(op),
131            ButtonAction::OpenParen => Some('('),
132            ButtonAction::CloseParen => Some(')'),
133            ButtonAction::Equals | ButtonAction::Clear => None,
134        }
135    }
136}
137
138/// The keypad layout - a 5x4 grid of buttons
139/// ```text
140/// [ 7 ] [ 8 ] [ 9 ] [ / ]
141/// [ 4 ] [ 5 ] [ 6 ] [ * ]
142/// [ 1 ] [ 2 ] [ 3 ] [ - ]
143/// [ 0 ] [ . ] [ = ] [ + ]
144/// [ C ] [ ( ] [ ) ] [ ^ ]
145/// ```
146#[derive(Debug, Clone)]
147pub struct Keypad {
148    /// Buttons in row-major order (5 rows x 4 cols)
149    buttons: Vec<KeypadButton>,
150    /// Number of columns
151    cols: usize,
152    /// Number of rows
153    rows: usize,
154}
155
156impl Default for Keypad {
157    fn default() -> Self {
158        Self::new()
159    }
160}
161
162impl Keypad {
163    /// Creates a new standard calculator keypad
164    #[must_use]
165    pub fn new() -> Self {
166        let buttons = vec![
167            // Row 1: 7 8 9 /
168            KeypadButton::digit(7),
169            KeypadButton::digit(8),
170            KeypadButton::digit(9),
171            KeypadButton::operator('/'),
172            // Row 2: 4 5 6 *
173            KeypadButton::digit(4),
174            KeypadButton::digit(5),
175            KeypadButton::digit(6),
176            KeypadButton::operator('*'),
177            // Row 3: 1 2 3 -
178            KeypadButton::digit(1),
179            KeypadButton::digit(2),
180            KeypadButton::digit(3),
181            KeypadButton::operator('-'),
182            // Row 4: 0 . = +
183            KeypadButton::digit(0),
184            KeypadButton::decimal(),
185            KeypadButton::equals(),
186            KeypadButton::operator('+'),
187            // Row 5: C ( ) ^
188            KeypadButton::clear(),
189            KeypadButton::open_paren(),
190            KeypadButton::close_paren(),
191            KeypadButton::operator('^'),
192        ];
193
194        Self {
195            buttons,
196            cols: 4,
197            rows: 5,
198        }
199    }
200
201    /// Returns the number of buttons
202    #[must_use]
203    pub fn button_count(&self) -> usize {
204        self.buttons.len()
205    }
206
207    /// Returns the grid dimensions (rows, cols)
208    #[must_use]
209    pub fn dimensions(&self) -> (usize, usize) {
210        (self.rows, self.cols)
211    }
212
213    /// Gets a button by index
214    #[must_use]
215    pub fn get_button(&self, index: usize) -> Option<&KeypadButton> {
216        self.buttons.get(index)
217    }
218
219    /// Gets a mutable button by index
220    pub fn get_button_mut(&mut self, index: usize) -> Option<&mut KeypadButton> {
221        self.buttons.get_mut(index)
222    }
223
224    /// Gets a button by row and column
225    #[must_use]
226    pub fn get_button_at(&self, row: usize, col: usize) -> Option<&KeypadButton> {
227        if row < self.rows && col < self.cols {
228            self.buttons.get(row * self.cols + col)
229        } else {
230            None
231        }
232    }
233
234    /// Finds a button by its label character
235    #[must_use]
236    pub fn find_button_by_label(&self, label: char) -> Option<usize> {
237        self.buttons.iter().position(|b| b.label == label)
238    }
239
240    /// Finds a button by the character it would insert
241    #[must_use]
242    pub fn find_button_by_char(&self, ch: char) -> Option<usize> {
243        self.buttons.iter().position(|b| b.to_char() == Some(ch))
244    }
245
246    /// Sets a button as pressed by index
247    pub fn press_button(&mut self, index: usize) {
248        if let Some(btn) = self.buttons.get_mut(index) {
249            btn.set_pressed(true);
250        }
251    }
252
253    /// Releases all buttons
254    pub fn release_all(&mut self) {
255        for btn in &mut self.buttons {
256            btn.set_pressed(false);
257        }
258    }
259
260    /// Highlights the button corresponding to a character
261    pub fn highlight_char(&mut self, ch: char) {
262        self.release_all();
263        if let Some(idx) = self.find_button_by_char(ch) {
264            self.press_button(idx);
265        }
266    }
267
268    /// Returns an iterator over all buttons
269    pub fn buttons(&self) -> impl Iterator<Item = &KeypadButton> {
270        self.buttons.iter()
271    }
272
273    /// Returns an iterator over buttons with their (row, col) positions
274    pub fn buttons_with_positions(&self) -> impl Iterator<Item = ((usize, usize), &KeypadButton)> {
275        self.buttons.iter().enumerate().map(move |(i, btn)| {
276            let row = i / self.cols;
277            let col = i % self.cols;
278            ((row, col), btn)
279        })
280    }
281
282    /// Converts a click position to button index
283    #[must_use]
284    pub fn hit_test(&self, area: Rect, x: u16, y: u16) -> Option<usize> {
285        if x < area.x || y < area.y || x >= area.x + area.width || y >= area.y + area.height {
286            return None;
287        }
288
289        let rel_x = x - area.x;
290        let rel_y = y - area.y;
291
292        // Account for border (1 char on each side)
293        if rel_x == 0 || rel_y == 0 || rel_x >= area.width - 1 || rel_y >= area.height - 1 {
294            return None;
295        }
296
297        let inner_x = rel_x - 1;
298        let inner_y = rel_y - 1;
299
300        let btn_width = (area.width - 2) / self.cols as u16;
301        let btn_height = (area.height - 2) / self.rows as u16;
302
303        if btn_width == 0 || btn_height == 0 {
304            return None;
305        }
306
307        let col = (inner_x / btn_width) as usize;
308        let row = (inner_y / btn_height) as usize;
309
310        if row < self.rows && col < self.cols {
311            Some(row * self.cols + col)
312        } else {
313            None
314        }
315    }
316}
317
318/// Keypad widget for rendering
319#[derive(Debug)]
320pub struct KeypadWidget<'a> {
321    keypad: &'a Keypad,
322}
323
324impl<'a> KeypadWidget<'a> {
325    /// Creates a new keypad widget
326    #[must_use]
327    pub fn new(keypad: &'a Keypad) -> Self {
328        Self { keypad }
329    }
330}
331
332impl Widget for KeypadWidget<'_> {
333    fn render(self, area: Rect, buf: &mut Buffer) {
334        // Draw border
335        Block::default()
336            .title(" Keypad ")
337            .borders(Borders::ALL)
338            .border_style(Style::default().fg(Color::Cyan))
339            .render(area, buf);
340
341        // Calculate inner area
342        let inner = Rect {
343            x: area.x + 1,
344            y: area.y + 1,
345            width: area.width.saturating_sub(2),
346            height: area.height.saturating_sub(2),
347        };
348
349        if inner.width < 4 || inner.height < 5 {
350            return; // Too small to render
351        }
352
353        let btn_width = inner.width / self.keypad.cols as u16;
354        let btn_height = inner.height / self.keypad.rows as u16;
355
356        for ((row, col), btn) in self.keypad.buttons_with_positions() {
357            let x = inner.x + (col as u16 * btn_width);
358            let y = inner.y + (row as u16 * btn_height);
359
360            // Button style based on pressed state
361            let style = if btn.pressed {
362                Style::default()
363                    .fg(Color::Black)
364                    .bg(Color::Yellow)
365                    .add_modifier(Modifier::BOLD)
366            } else {
367                match btn.action {
368                    ButtonAction::Digit(_) => Style::default().fg(Color::White),
369                    ButtonAction::Operator(_) => Style::default().fg(Color::Yellow),
370                    ButtonAction::Equals => Style::default().fg(Color::Green),
371                    ButtonAction::Clear => Style::default().fg(Color::Red),
372                    _ => Style::default().fg(Color::Cyan),
373                }
374            };
375
376            // Render button label centered
377            if btn_width >= 3 {
378                let label = format!("[{}]", btn.label);
379                let label_x = x + (btn_width.saturating_sub(label.len() as u16)) / 2;
380                let label_y = y + btn_height / 2;
381
382                if label_y < inner.y + inner.height && label_x < inner.x + inner.width {
383                    buf.set_span(label_x, label_y, &Span::styled(label, style), btn_width);
384                }
385            }
386        }
387    }
388}
389
390#[cfg(test)]
391mod tests {
392    use super::*;
393
394    // ===== KeypadButton tests =====
395
396    #[test]
397    fn test_digit_button_creation() {
398        for d in 0..=9 {
399            let btn = KeypadButton::digit(d);
400            assert_eq!(btn.label, char::from_digit(d as u32, 10).unwrap());
401            assert!(!btn.pressed);
402            assert_eq!(btn.action, ButtonAction::Digit(d));
403        }
404    }
405
406    #[test]
407    fn test_operator_button_creation() {
408        for op in ['+', '-', '*', '/', '^'] {
409            let btn = KeypadButton::operator(op);
410            assert_eq!(btn.label, op);
411            assert!(!btn.pressed);
412            assert_eq!(btn.action, ButtonAction::Operator(op));
413        }
414    }
415
416    #[test]
417    fn test_decimal_button() {
418        let btn = KeypadButton::decimal();
419        assert_eq!(btn.label, '.');
420        assert_eq!(btn.action, ButtonAction::Decimal);
421    }
422
423    #[test]
424    fn test_equals_button() {
425        let btn = KeypadButton::equals();
426        assert_eq!(btn.label, '=');
427        assert_eq!(btn.action, ButtonAction::Equals);
428    }
429
430    #[test]
431    fn test_clear_button() {
432        let btn = KeypadButton::clear();
433        assert_eq!(btn.label, 'C');
434        assert_eq!(btn.action, ButtonAction::Clear);
435    }
436
437    #[test]
438    fn test_paren_buttons() {
439        let open = KeypadButton::open_paren();
440        assert_eq!(open.label, '(');
441        assert_eq!(open.action, ButtonAction::OpenParen);
442
443        let close = KeypadButton::close_paren();
444        assert_eq!(close.label, ')');
445        assert_eq!(close.action, ButtonAction::CloseParen);
446    }
447
448    #[test]
449    fn test_button_pressed_state() {
450        let mut btn = KeypadButton::digit(5);
451        assert!(!btn.pressed);
452        btn.set_pressed(true);
453        assert!(btn.pressed);
454        btn.set_pressed(false);
455        assert!(!btn.pressed);
456    }
457
458    #[test]
459    fn test_button_to_char() {
460        assert_eq!(KeypadButton::digit(5).to_char(), Some('5'));
461        assert_eq!(KeypadButton::decimal().to_char(), Some('.'));
462        assert_eq!(KeypadButton::operator('+').to_char(), Some('+'));
463        assert_eq!(KeypadButton::open_paren().to_char(), Some('('));
464        assert_eq!(KeypadButton::close_paren().to_char(), Some(')'));
465        assert_eq!(KeypadButton::equals().to_char(), None);
466        assert_eq!(KeypadButton::clear().to_char(), None);
467    }
468
469    #[test]
470    fn test_button_clone() {
471        let btn = KeypadButton::digit(7);
472        let cloned = btn.clone();
473        assert_eq!(btn, cloned);
474    }
475
476    #[test]
477    fn test_button_debug() {
478        let btn = KeypadButton::digit(9);
479        let debug = format!("{:?}", btn);
480        assert!(debug.contains("KeypadButton"));
481    }
482
483    // ===== ButtonAction tests =====
484
485    #[test]
486    fn test_button_action_copy() {
487        let action = ButtonAction::Digit(5);
488        let copied = action;
489        assert_eq!(action, copied);
490    }
491
492    #[test]
493    fn test_button_action_debug() {
494        let action = ButtonAction::Operator('+');
495        let debug = format!("{:?}", action);
496        assert!(debug.contains("Operator"));
497    }
498
499    // ===== Keypad tests =====
500
501    #[test]
502    fn test_keypad_new() {
503        let keypad = Keypad::new();
504        assert_eq!(keypad.button_count(), 20); // 5 rows x 4 cols
505    }
506
507    #[test]
508    fn test_keypad_default() {
509        let keypad = Keypad::default();
510        assert_eq!(keypad.button_count(), 20);
511    }
512
513    #[test]
514    fn test_keypad_dimensions() {
515        let keypad = Keypad::new();
516        assert_eq!(keypad.dimensions(), (5, 4));
517    }
518
519    #[test]
520    fn test_keypad_get_button() {
521        let keypad = Keypad::new();
522        // First button should be 7
523        let btn = keypad.get_button(0).unwrap();
524        assert_eq!(btn.label, '7');
525    }
526
527    #[test]
528    fn test_keypad_get_button_out_of_bounds() {
529        let keypad = Keypad::new();
530        assert!(keypad.get_button(100).is_none());
531    }
532
533    #[test]
534    fn test_keypad_get_button_at() {
535        let keypad = Keypad::new();
536        // Row 0, Col 0 = 7
537        assert_eq!(keypad.get_button_at(0, 0).unwrap().label, '7');
538        // Row 0, Col 3 = /
539        assert_eq!(keypad.get_button_at(0, 3).unwrap().label, '/');
540        // Row 4, Col 0 = C
541        assert_eq!(keypad.get_button_at(4, 0).unwrap().label, 'C');
542    }
543
544    #[test]
545    fn test_keypad_get_button_at_out_of_bounds() {
546        let keypad = Keypad::new();
547        assert!(keypad.get_button_at(10, 10).is_none());
548    }
549
550    #[test]
551    fn test_keypad_find_by_label() {
552        let keypad = Keypad::new();
553        assert_eq!(keypad.find_button_by_label('7'), Some(0));
554        assert_eq!(keypad.find_button_by_label('0'), Some(12));
555        assert_eq!(keypad.find_button_by_label('='), Some(14));
556        assert_eq!(keypad.find_button_by_label('X'), None);
557    }
558
559    #[test]
560    fn test_keypad_find_by_char() {
561        let keypad = Keypad::new();
562        assert_eq!(keypad.find_button_by_char('5'), Some(5));
563        assert_eq!(keypad.find_button_by_char('+'), Some(15));
564        assert_eq!(keypad.find_button_by_char('.'), Some(13));
565    }
566
567    #[test]
568    fn test_keypad_press_button() {
569        let mut keypad = Keypad::new();
570        keypad.press_button(0);
571        assert!(keypad.get_button(0).unwrap().pressed);
572        assert!(!keypad.get_button(1).unwrap().pressed);
573    }
574
575    #[test]
576    fn test_keypad_release_all() {
577        let mut keypad = Keypad::new();
578        keypad.press_button(0);
579        keypad.press_button(5);
580        keypad.release_all();
581        for btn in keypad.buttons() {
582            assert!(!btn.pressed);
583        }
584    }
585
586    #[test]
587    fn test_keypad_highlight_char() {
588        let mut keypad = Keypad::new();
589        keypad.highlight_char('5');
590        assert!(keypad.get_button(5).unwrap().pressed);
591        // Other buttons should not be pressed
592        assert!(!keypad.get_button(0).unwrap().pressed);
593    }
594
595    #[test]
596    fn test_keypad_buttons_iterator() {
597        let keypad = Keypad::new();
598        let count = keypad.buttons().count();
599        assert_eq!(count, 20);
600    }
601
602    #[test]
603    fn test_keypad_buttons_with_positions() {
604        let keypad = Keypad::new();
605        let positions: Vec<_> = keypad.buttons_with_positions().collect();
606        assert_eq!(positions.len(), 20);
607        assert_eq!(positions[0].0, (0, 0)); // First button at row 0, col 0
608        assert_eq!(positions[19].0, (4, 3)); // Last button at row 4, col 3
609    }
610
611    #[test]
612    fn test_keypad_hit_test_inside() {
613        let keypad = Keypad::new();
614        let area = Rect::new(0, 0, 22, 12); // Big enough for 4x5 grid
615
616        // Click in center should hit a button
617        let result = keypad.hit_test(area, 10, 5);
618        assert!(result.is_some());
619    }
620
621    #[test]
622    fn test_keypad_hit_test_outside() {
623        let keypad = Keypad::new();
624        let area = Rect::new(10, 10, 22, 12);
625
626        // Click outside area
627        assert!(keypad.hit_test(area, 0, 0).is_none());
628        assert!(keypad.hit_test(area, 100, 100).is_none());
629    }
630
631    #[test]
632    fn test_keypad_hit_test_border() {
633        let keypad = Keypad::new();
634        let area = Rect::new(0, 0, 22, 12);
635
636        // Click on border (first row/col)
637        assert!(keypad.hit_test(area, 0, 0).is_none());
638    }
639
640    #[test]
641    fn test_keypad_get_button_mut() {
642        let mut keypad = Keypad::new();
643        if let Some(btn) = keypad.get_button_mut(0) {
644            btn.set_pressed(true);
645        }
646        assert!(keypad.get_button(0).unwrap().pressed);
647    }
648
649    #[test]
650    fn test_keypad_clone() {
651        let keypad = Keypad::new();
652        let cloned = keypad.clone();
653        assert_eq!(keypad.button_count(), cloned.button_count());
654    }
655
656    #[test]
657    fn test_keypad_debug() {
658        let keypad = Keypad::new();
659        let debug = format!("{:?}", keypad);
660        assert!(debug.contains("Keypad"));
661    }
662
663    // ===== Keypad layout verification =====
664
665    #[test]
666    fn test_keypad_row_1() {
667        let keypad = Keypad::new();
668        assert_eq!(keypad.get_button_at(0, 0).unwrap().label, '7');
669        assert_eq!(keypad.get_button_at(0, 1).unwrap().label, '8');
670        assert_eq!(keypad.get_button_at(0, 2).unwrap().label, '9');
671        assert_eq!(keypad.get_button_at(0, 3).unwrap().label, '/');
672    }
673
674    #[test]
675    fn test_keypad_row_2() {
676        let keypad = Keypad::new();
677        assert_eq!(keypad.get_button_at(1, 0).unwrap().label, '4');
678        assert_eq!(keypad.get_button_at(1, 1).unwrap().label, '5');
679        assert_eq!(keypad.get_button_at(1, 2).unwrap().label, '6');
680        assert_eq!(keypad.get_button_at(1, 3).unwrap().label, '*');
681    }
682
683    #[test]
684    fn test_keypad_row_3() {
685        let keypad = Keypad::new();
686        assert_eq!(keypad.get_button_at(2, 0).unwrap().label, '1');
687        assert_eq!(keypad.get_button_at(2, 1).unwrap().label, '2');
688        assert_eq!(keypad.get_button_at(2, 2).unwrap().label, '3');
689        assert_eq!(keypad.get_button_at(2, 3).unwrap().label, '-');
690    }
691
692    #[test]
693    fn test_keypad_row_4() {
694        let keypad = Keypad::new();
695        assert_eq!(keypad.get_button_at(3, 0).unwrap().label, '0');
696        assert_eq!(keypad.get_button_at(3, 1).unwrap().label, '.');
697        assert_eq!(keypad.get_button_at(3, 2).unwrap().label, '=');
698        assert_eq!(keypad.get_button_at(3, 3).unwrap().label, '+');
699    }
700
701    #[test]
702    fn test_keypad_row_5() {
703        let keypad = Keypad::new();
704        assert_eq!(keypad.get_button_at(4, 0).unwrap().label, 'C');
705        assert_eq!(keypad.get_button_at(4, 1).unwrap().label, '(');
706        assert_eq!(keypad.get_button_at(4, 2).unwrap().label, ')');
707        assert_eq!(keypad.get_button_at(4, 3).unwrap().label, '^');
708    }
709
710    // ===== KeypadWidget tests =====
711
712    #[test]
713    fn test_keypad_widget_new() {
714        let keypad = Keypad::new();
715        let widget = KeypadWidget::new(&keypad);
716        // Just verify it creates without panic
717        let _ = widget;
718    }
719
720    #[test]
721    fn test_keypad_widget_render() {
722        let keypad = Keypad::new();
723        let widget = KeypadWidget::new(&keypad);
724        let area = Rect::new(0, 0, 22, 12);
725        let mut buf = Buffer::empty(area);
726
727        widget.render(area, &mut buf);
728
729        let content: String = buf.content().iter().map(|c| c.symbol()).collect();
730        assert!(content.contains("Keypad"));
731        assert!(content.contains("[7]"));
732        assert!(content.contains("[+]"));
733    }
734
735    #[test]
736    fn test_keypad_widget_render_small() {
737        let keypad = Keypad::new();
738        let widget = KeypadWidget::new(&keypad);
739        let area = Rect::new(0, 0, 5, 5); // Too small
740        let mut buf = Buffer::empty(area);
741
742        // Should not panic, just render border
743        widget.render(area, &mut buf);
744    }
745
746    #[test]
747    fn test_keypad_widget_render_pressed() {
748        let mut keypad = Keypad::new();
749        keypad.press_button(0); // Press '7'
750        let widget = KeypadWidget::new(&keypad);
751        let area = Rect::new(0, 0, 22, 12);
752        let mut buf = Buffer::empty(area);
753
754        widget.render(area, &mut buf);
755        // Pressed button should still render
756        let content: String = buf.content().iter().map(|c| c.symbol()).collect();
757        assert!(content.contains("[7]"));
758    }
759
760    // ===== Property-based tests =====
761
762    #[test]
763    fn prop_all_digits_have_buttons() {
764        let keypad = Keypad::new();
765        for d in 0..=9 {
766            let ch = char::from_digit(d, 10).unwrap();
767            assert!(
768                keypad.find_button_by_char(ch).is_some(),
769                "Missing button for digit {d}"
770            );
771        }
772    }
773
774    #[test]
775    fn prop_all_operators_have_buttons() {
776        let keypad = Keypad::new();
777        for op in ['+', '-', '*', '/', '^'] {
778            assert!(
779                keypad.find_button_by_char(op).is_some(),
780                "Missing button for operator {op}"
781            );
782        }
783    }
784
785    #[test]
786    fn prop_button_char_roundtrip() {
787        let keypad = Keypad::new();
788        for btn in keypad.buttons() {
789            if let Some(ch) = btn.to_char() {
790                // Should be able to find it back
791                let found = keypad.find_button_by_char(ch);
792                assert!(found.is_some(), "Cannot find button for char '{ch}'");
793            }
794        }
795    }
796
797    #[test]
798    fn prop_press_release_idempotent() {
799        let mut keypad = Keypad::new();
800        keypad.press_button(5);
801        keypad.press_button(5); // Press again
802        assert!(keypad.get_button(5).unwrap().pressed);
803
804        keypad.release_all();
805        keypad.release_all(); // Release again
806        for btn in keypad.buttons() {
807            assert!(!btn.pressed);
808        }
809    }
810
811    #[test]
812    fn prop_highlight_releases_others() {
813        let mut keypad = Keypad::new();
814        keypad.press_button(0);
815        keypad.press_button(5);
816        keypad.press_button(10);
817
818        keypad.highlight_char('1'); // Should release all and press only '1'
819
820        let pressed_count = keypad.buttons().filter(|b| b.pressed).count();
821        assert_eq!(pressed_count, 1);
822    }
823}