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