1use ratatui::{
11 buffer::Buffer,
12 layout::Rect,
13 style::{Color, Modifier, Style},
14 text::Span,
15 widgets::{Block, Borders, Widget},
16};
17
18#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct KeypadButton {
21 pub label: char,
23 pub pressed: bool,
25 pub action: ButtonAction,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum ButtonAction {
32 Digit(u8),
34 Decimal,
36 Operator(char),
38 Equals,
40 Clear,
42 OpenParen,
44 CloseParen,
46}
47
48impl KeypadButton {
49 #[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 #[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 #[must_use]
71 pub fn decimal() -> Self {
72 Self {
73 label: '.',
74 pressed: false,
75 action: ButtonAction::Decimal,
76 }
77 }
78
79 #[must_use]
81 pub fn equals() -> Self {
82 Self {
83 label: '=',
84 pressed: false,
85 action: ButtonAction::Equals,
86 }
87 }
88
89 #[must_use]
91 pub fn clear() -> Self {
92 Self {
93 label: 'C',
94 pressed: false,
95 action: ButtonAction::Clear,
96 }
97 }
98
99 #[must_use]
101 pub fn open_paren() -> Self {
102 Self {
103 label: '(',
104 pressed: false,
105 action: ButtonAction::OpenParen,
106 }
107 }
108
109 #[must_use]
111 pub fn close_paren() -> Self {
112 Self {
113 label: ')',
114 pressed: false,
115 action: ButtonAction::CloseParen,
116 }
117 }
118
119 pub fn set_pressed(&mut self, pressed: bool) {
121 self.pressed = pressed;
122 }
123
124 #[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#[derive(Debug, Clone)]
147pub struct Keypad {
148 buttons: Vec<KeypadButton>,
150 cols: usize,
152 rows: usize,
154}
155
156impl Default for Keypad {
157 fn default() -> Self {
158 Self::new()
159 }
160}
161
162impl Keypad {
163 #[must_use]
165 pub fn new() -> Self {
166 let buttons = vec![
167 KeypadButton::digit(7),
169 KeypadButton::digit(8),
170 KeypadButton::digit(9),
171 KeypadButton::operator('/'),
172 KeypadButton::digit(4),
174 KeypadButton::digit(5),
175 KeypadButton::digit(6),
176 KeypadButton::operator('*'),
177 KeypadButton::digit(1),
179 KeypadButton::digit(2),
180 KeypadButton::digit(3),
181 KeypadButton::operator('-'),
182 KeypadButton::digit(0),
184 KeypadButton::decimal(),
185 KeypadButton::equals(),
186 KeypadButton::operator('+'),
187 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 #[must_use]
203 pub fn button_count(&self) -> usize {
204 self.buttons.len()
205 }
206
207 #[must_use]
209 pub fn dimensions(&self) -> (usize, usize) {
210 (self.rows, self.cols)
211 }
212
213 #[must_use]
215 pub fn get_button(&self, index: usize) -> Option<&KeypadButton> {
216 self.buttons.get(index)
217 }
218
219 pub fn get_button_mut(&mut self, index: usize) -> Option<&mut KeypadButton> {
221 self.buttons.get_mut(index)
222 }
223
224 #[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 #[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 #[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 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 pub fn release_all(&mut self) {
255 for btn in &mut self.buttons {
256 btn.set_pressed(false);
257 }
258 }
259
260 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 pub fn buttons(&self) -> impl Iterator<Item = &KeypadButton> {
270 self.buttons.iter()
271 }
272
273 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 #[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 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#[derive(Debug)]
320pub struct KeypadWidget<'a> {
321 keypad: &'a Keypad,
322}
323
324impl<'a> KeypadWidget<'a> {
325 #[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 Block::default()
336 .title(" Keypad ")
337 .borders(Borders::ALL)
338 .border_style(Style::default().fg(Color::Cyan))
339 .render(area, buf);
340
341 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; }
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 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 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 #[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 #[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 #[test]
502 fn test_keypad_new() {
503 let keypad = Keypad::new();
504 assert_eq!(keypad.button_count(), 20); }
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 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 assert_eq!(keypad.get_button_at(0, 0).unwrap().label, '7');
538 assert_eq!(keypad.get_button_at(0, 3).unwrap().label, '/');
540 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 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)); assert_eq!(positions[19].0, (4, 3)); }
610
611 #[test]
612 fn test_keypad_hit_test_inside() {
613 let keypad = Keypad::new();
614 let area = Rect::new(0, 0, 22, 12); 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 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 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 #[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 #[test]
713 fn test_keypad_widget_new() {
714 let keypad = Keypad::new();
715 let widget = KeypadWidget::new(&keypad);
716 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); let mut buf = Buffer::empty(area);
741
742 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); 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 let content: String = buf.content().iter().map(|c| c.symbol()).collect();
757 assert!(content.contains("[7]"));
758 }
759
760 #[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 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); assert!(keypad.get_button(5).unwrap().pressed);
803
804 keypad.release_all();
805 keypad.release_all(); 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'); let pressed_count = keypad.buttons().filter(|b| b.pressed).count();
821 assert_eq!(pressed_count, 1);
822 }
823}