1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub struct Rect {
13 pub x: u16,
15 pub y: u16,
17 pub width: u16,
19 pub height: u16,
21}
22
23impl Rect {
24 #[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#[derive(Debug, Clone)]
38pub struct TextBuffer {
39 cells: Vec<char>,
40 width: u16,
41 height: u16,
42}
43
44impl TextBuffer {
45 #[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 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 #[must_use]
73 pub fn to_string_content(&self) -> String {
74 self.cells.iter().collect()
75 }
76
77 #[must_use]
79 pub fn contains(&self, text: &str) -> bool {
80 self.to_string_content().contains(text)
81 }
82}
83
84#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct KeypadButton {
87 pub label: char,
89 pub pressed: bool,
91 pub action: ButtonAction,
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub enum ButtonAction {
98 Digit(u8),
100 Decimal,
102 Operator(char),
104 Equals,
106 Clear,
108 OpenParen,
110 CloseParen,
112}
113
114impl KeypadButton {
115 #[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 #[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 #[must_use]
137 pub fn decimal() -> Self {
138 Self {
139 label: '.',
140 pressed: false,
141 action: ButtonAction::Decimal,
142 }
143 }
144
145 #[must_use]
147 pub fn equals() -> Self {
148 Self {
149 label: '=',
150 pressed: false,
151 action: ButtonAction::Equals,
152 }
153 }
154
155 #[must_use]
157 pub fn clear() -> Self {
158 Self {
159 label: 'C',
160 pressed: false,
161 action: ButtonAction::Clear,
162 }
163 }
164
165 #[must_use]
167 pub fn open_paren() -> Self {
168 Self {
169 label: '(',
170 pressed: false,
171 action: ButtonAction::OpenParen,
172 }
173 }
174
175 #[must_use]
177 pub fn close_paren() -> Self {
178 Self {
179 label: ')',
180 pressed: false,
181 action: ButtonAction::CloseParen,
182 }
183 }
184
185 pub fn set_pressed(&mut self, pressed: bool) {
187 self.pressed = pressed;
188 }
189
190 #[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#[derive(Debug, Clone)]
213pub struct Keypad {
214 buttons: Vec<KeypadButton>,
216 cols: usize,
218 rows: usize,
220}
221
222impl Default for Keypad {
223 fn default() -> Self {
224 Self::new()
225 }
226}
227
228impl Keypad {
229 #[must_use]
231 pub fn new() -> Self {
232 let buttons = vec![
233 KeypadButton::digit(7),
235 KeypadButton::digit(8),
236 KeypadButton::digit(9),
237 KeypadButton::operator('/'),
238 KeypadButton::digit(4),
240 KeypadButton::digit(5),
241 KeypadButton::digit(6),
242 KeypadButton::operator('*'),
243 KeypadButton::digit(1),
245 KeypadButton::digit(2),
246 KeypadButton::digit(3),
247 KeypadButton::operator('-'),
248 KeypadButton::digit(0),
250 KeypadButton::decimal(),
251 KeypadButton::equals(),
252 KeypadButton::operator('+'),
253 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 #[must_use]
269 pub fn button_count(&self) -> usize {
270 self.buttons.len()
271 }
272
273 #[must_use]
275 pub fn dimensions(&self) -> (usize, usize) {
276 (self.rows, self.cols)
277 }
278
279 #[must_use]
281 pub fn get_button(&self, index: usize) -> Option<&KeypadButton> {
282 self.buttons.get(index)
283 }
284
285 pub fn get_button_mut(&mut self, index: usize) -> Option<&mut KeypadButton> {
287 self.buttons.get_mut(index)
288 }
289
290 #[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 #[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 #[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 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 pub fn release_all(&mut self) {
321 for btn in &mut self.buttons {
322 btn.set_pressed(false);
323 }
324 }
325
326 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 pub fn buttons(&self) -> impl Iterator<Item = &KeypadButton> {
336 self.buttons.iter()
337 }
338
339 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 #[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 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#[derive(Debug)]
386pub struct KeypadWidget<'a> {
387 keypad: &'a Keypad,
388}
389
390impl<'a> KeypadWidget<'a> {
391 #[must_use]
393 pub fn new(keypad: &'a Keypad) -> Self {
394 Self { keypad }
395 }
396
397 pub fn render(&self, area: Rect, buf: &mut TextBuffer) {
399 if area.width >= 2 && area.height >= 2 {
401 buf.write_str(area.x + 1, area.y, " Keypad ");
402 }
403
404 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; }
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 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 #[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 #[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 #[test]
547 fn test_keypad_new() {
548 let keypad = Keypad::new();
549 assert_eq!(keypad.button_count(), 20); }
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 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 assert_eq!(keypad.get_button_at(0, 0).unwrap().label, '7');
583 assert_eq!(keypad.get_button_at(0, 3).unwrap().label, '/');
585 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 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)); assert_eq!(positions[19].0, (4, 3)); }
655
656 #[test]
657 fn test_keypad_hit_test_inside() {
658 let keypad = Keypad::new();
659 let area = Rect::new(0, 0, 22, 12); 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 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 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 #[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 #[test]
758 fn test_keypad_widget_new() {
759 let keypad = Keypad::new();
760 let widget = KeypadWidget::new(&keypad);
761 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); let mut buf = TextBuffer::empty(area);
786
787 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); 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 let content = buf.to_string_content();
802 assert!(content.contains("[7]"));
803 }
804
805 #[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 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); assert!(keypad.get_button(5).unwrap().pressed);
848
849 keypad.release_all();
850 keypad.release_all(); 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'); let pressed_count = keypad.buttons().filter(|b| b.pressed).count();
866 assert_eq!(pressed_count, 1);
867 }
868}