#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Rect {
pub x: u16,
pub y: u16,
pub width: u16,
pub height: u16,
}
impl Rect {
#[must_use]
pub fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
Self {
x,
y,
width,
height,
}
}
}
#[derive(Debug, Clone)]
pub struct TextBuffer {
cells: Vec<char>,
width: u16,
height: u16,
}
impl TextBuffer {
#[must_use]
pub fn empty(area: Rect) -> Self {
let size = (area.width as usize) * (area.height as usize);
Self {
cells: vec![' '; size],
width: area.width,
height: area.height,
}
}
pub fn write_str(&mut self, x: u16, y: u16, s: &str) {
let mut cx = x;
for ch in s.chars() {
if cx >= self.width || y >= self.height {
break;
}
let idx = (y as usize) * (self.width as usize) + (cx as usize);
if idx < self.cells.len() {
self.cells[idx] = ch;
}
cx += 1;
}
}
#[must_use]
pub fn to_string_content(&self) -> String {
self.cells.iter().collect()
}
#[must_use]
pub fn contains(&self, text: &str) -> bool {
self.to_string_content().contains(text)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KeypadButton {
pub label: char,
pub pressed: bool,
pub action: ButtonAction,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ButtonAction {
Digit(u8),
Decimal,
Operator(char),
Equals,
Clear,
OpenParen,
CloseParen,
}
impl KeypadButton {
#[must_use]
pub fn digit(d: u8) -> Self {
Self {
label: char::from_digit(d as u32, 10).unwrap_or('?'),
pressed: false,
action: ButtonAction::Digit(d),
}
}
#[must_use]
pub fn operator(op: char) -> Self {
Self {
label: op,
pressed: false,
action: ButtonAction::Operator(op),
}
}
#[must_use]
pub fn decimal() -> Self {
Self {
label: '.',
pressed: false,
action: ButtonAction::Decimal,
}
}
#[must_use]
pub fn equals() -> Self {
Self {
label: '=',
pressed: false,
action: ButtonAction::Equals,
}
}
#[must_use]
pub fn clear() -> Self {
Self {
label: 'C',
pressed: false,
action: ButtonAction::Clear,
}
}
#[must_use]
pub fn open_paren() -> Self {
Self {
label: '(',
pressed: false,
action: ButtonAction::OpenParen,
}
}
#[must_use]
pub fn close_paren() -> Self {
Self {
label: ')',
pressed: false,
action: ButtonAction::CloseParen,
}
}
pub fn set_pressed(&mut self, pressed: bool) {
self.pressed = pressed;
}
#[must_use]
pub fn to_char(&self) -> Option<char> {
match self.action {
ButtonAction::Digit(d) => char::from_digit(d as u32, 10),
ButtonAction::Decimal => Some('.'),
ButtonAction::Operator(op) => Some(op),
ButtonAction::OpenParen => Some('('),
ButtonAction::CloseParen => Some(')'),
ButtonAction::Equals | ButtonAction::Clear => None,
}
}
}
#[derive(Debug, Clone)]
pub struct Keypad {
buttons: Vec<KeypadButton>,
cols: usize,
rows: usize,
}
impl Default for Keypad {
fn default() -> Self {
Self::new()
}
}
impl Keypad {
#[must_use]
pub fn new() -> Self {
let buttons = vec![
KeypadButton::digit(7),
KeypadButton::digit(8),
KeypadButton::digit(9),
KeypadButton::operator('/'),
KeypadButton::digit(4),
KeypadButton::digit(5),
KeypadButton::digit(6),
KeypadButton::operator('*'),
KeypadButton::digit(1),
KeypadButton::digit(2),
KeypadButton::digit(3),
KeypadButton::operator('-'),
KeypadButton::digit(0),
KeypadButton::decimal(),
KeypadButton::equals(),
KeypadButton::operator('+'),
KeypadButton::clear(),
KeypadButton::open_paren(),
KeypadButton::close_paren(),
KeypadButton::operator('^'),
];
Self {
buttons,
cols: 4,
rows: 5,
}
}
#[must_use]
pub fn button_count(&self) -> usize {
self.buttons.len()
}
#[must_use]
pub fn dimensions(&self) -> (usize, usize) {
(self.rows, self.cols)
}
#[must_use]
pub fn get_button(&self, index: usize) -> Option<&KeypadButton> {
self.buttons.get(index)
}
pub fn get_button_mut(&mut self, index: usize) -> Option<&mut KeypadButton> {
self.buttons.get_mut(index)
}
#[must_use]
pub fn get_button_at(&self, row: usize, col: usize) -> Option<&KeypadButton> {
if row < self.rows && col < self.cols {
self.buttons.get(row * self.cols + col)
} else {
None
}
}
#[must_use]
pub fn find_button_by_label(&self, label: char) -> Option<usize> {
self.buttons.iter().position(|b| b.label == label)
}
#[must_use]
pub fn find_button_by_char(&self, ch: char) -> Option<usize> {
self.buttons.iter().position(|b| b.to_char() == Some(ch))
}
pub fn press_button(&mut self, index: usize) {
if let Some(btn) = self.buttons.get_mut(index) {
btn.set_pressed(true);
}
}
pub fn release_all(&mut self) {
for btn in &mut self.buttons {
btn.set_pressed(false);
}
}
pub fn highlight_char(&mut self, ch: char) {
self.release_all();
if let Some(idx) = self.find_button_by_char(ch) {
self.press_button(idx);
}
}
pub fn buttons(&self) -> impl Iterator<Item = &KeypadButton> {
self.buttons.iter()
}
pub fn buttons_with_positions(&self) -> impl Iterator<Item = ((usize, usize), &KeypadButton)> {
self.buttons.iter().enumerate().map(move |(i, btn)| {
let row = i / self.cols;
let col = i % self.cols;
((row, col), btn)
})
}
#[must_use]
pub fn hit_test(&self, area: Rect, x: u16, y: u16) -> Option<usize> {
if x < area.x || y < area.y || x >= area.x + area.width || y >= area.y + area.height {
return None;
}
let rel_x = x - area.x;
let rel_y = y - area.y;
if rel_x == 0 || rel_y == 0 || rel_x >= area.width - 1 || rel_y >= area.height - 1 {
return None;
}
let inner_x = rel_x - 1;
let inner_y = rel_y - 1;
let btn_width = (area.width - 2) / self.cols as u16;
let btn_height = (area.height - 2) / self.rows as u16;
if btn_width == 0 || btn_height == 0 {
return None;
}
let col = (inner_x / btn_width) as usize;
let row = (inner_y / btn_height) as usize;
if row < self.rows && col < self.cols {
Some(row * self.cols + col)
} else {
None
}
}
}
#[derive(Debug)]
pub struct KeypadWidget<'a> {
keypad: &'a Keypad,
}
impl<'a> KeypadWidget<'a> {
#[must_use]
pub fn new(keypad: &'a Keypad) -> Self {
Self { keypad }
}
pub fn render(&self, area: Rect, buf: &mut TextBuffer) {
if area.width >= 2 && area.height >= 2 {
buf.write_str(area.x + 1, area.y, " Keypad ");
}
let inner_x = area.x + 1;
let inner_y = area.y + 1;
let inner_w = area.width.saturating_sub(2);
let inner_h = area.height.saturating_sub(2);
if inner_w < 4 || inner_h < 5 {
return; }
let btn_width = inner_w / self.keypad.cols as u16;
let btn_height = inner_h / self.keypad.rows as u16;
for ((row, col), btn) in self.keypad.buttons_with_positions() {
let x = inner_x + (col as u16 * btn_width);
let y = inner_y + (row as u16 * btn_height);
if btn_width >= 3 {
let label = format!("[{}]", btn.label);
let label_x = x + (btn_width.saturating_sub(label.len() as u16)) / 2;
let label_y = y + btn_height / 2;
if label_y < inner_y + inner_h && label_x < inner_x + inner_w {
buf.write_str(label_x, label_y, &label);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_digit_button_creation() {
for d in 0..=9 {
let btn = KeypadButton::digit(d);
assert_eq!(btn.label, char::from_digit(d as u32, 10).unwrap());
assert!(!btn.pressed);
assert_eq!(btn.action, ButtonAction::Digit(d));
}
}
#[test]
fn test_operator_button_creation() {
for op in ['+', '-', '*', '/', '^'] {
let btn = KeypadButton::operator(op);
assert_eq!(btn.label, op);
assert!(!btn.pressed);
assert_eq!(btn.action, ButtonAction::Operator(op));
}
}
#[test]
fn test_decimal_button() {
let btn = KeypadButton::decimal();
assert_eq!(btn.label, '.');
assert_eq!(btn.action, ButtonAction::Decimal);
}
#[test]
fn test_equals_button() {
let btn = KeypadButton::equals();
assert_eq!(btn.label, '=');
assert_eq!(btn.action, ButtonAction::Equals);
}
#[test]
fn test_clear_button() {
let btn = KeypadButton::clear();
assert_eq!(btn.label, 'C');
assert_eq!(btn.action, ButtonAction::Clear);
}
#[test]
fn test_paren_buttons() {
let open = KeypadButton::open_paren();
assert_eq!(open.label, '(');
assert_eq!(open.action, ButtonAction::OpenParen);
let close = KeypadButton::close_paren();
assert_eq!(close.label, ')');
assert_eq!(close.action, ButtonAction::CloseParen);
}
#[test]
fn test_button_pressed_state() {
let mut btn = KeypadButton::digit(5);
assert!(!btn.pressed);
btn.set_pressed(true);
assert!(btn.pressed);
btn.set_pressed(false);
assert!(!btn.pressed);
}
#[test]
fn test_button_to_char() {
assert_eq!(KeypadButton::digit(5).to_char(), Some('5'));
assert_eq!(KeypadButton::decimal().to_char(), Some('.'));
assert_eq!(KeypadButton::operator('+').to_char(), Some('+'));
assert_eq!(KeypadButton::open_paren().to_char(), Some('('));
assert_eq!(KeypadButton::close_paren().to_char(), Some(')'));
assert_eq!(KeypadButton::equals().to_char(), None);
assert_eq!(KeypadButton::clear().to_char(), None);
}
#[test]
fn test_button_clone() {
let btn = KeypadButton::digit(7);
let cloned = btn.clone();
assert_eq!(btn, cloned);
}
#[test]
fn test_button_debug() {
let btn = KeypadButton::digit(9);
let debug = format!("{:?}", btn);
assert!(debug.contains("KeypadButton"));
}
#[test]
fn test_button_action_copy() {
let action = ButtonAction::Digit(5);
let copied = action;
assert_eq!(action, copied);
}
#[test]
fn test_button_action_debug() {
let action = ButtonAction::Operator('+');
let debug = format!("{:?}", action);
assert!(debug.contains("Operator"));
}
#[test]
fn test_keypad_new() {
let keypad = Keypad::new();
assert_eq!(keypad.button_count(), 20); }
#[test]
fn test_keypad_default() {
let keypad = Keypad::default();
assert_eq!(keypad.button_count(), 20);
}
#[test]
fn test_keypad_dimensions() {
let keypad = Keypad::new();
assert_eq!(keypad.dimensions(), (5, 4));
}
#[test]
fn test_keypad_get_button() {
let keypad = Keypad::new();
let btn = keypad.get_button(0).unwrap();
assert_eq!(btn.label, '7');
}
#[test]
fn test_keypad_get_button_out_of_bounds() {
let keypad = Keypad::new();
assert!(keypad.get_button(100).is_none());
}
#[test]
fn test_keypad_get_button_at() {
let keypad = Keypad::new();
assert_eq!(keypad.get_button_at(0, 0).unwrap().label, '7');
assert_eq!(keypad.get_button_at(0, 3).unwrap().label, '/');
assert_eq!(keypad.get_button_at(4, 0).unwrap().label, 'C');
}
#[test]
fn test_keypad_get_button_at_out_of_bounds() {
let keypad = Keypad::new();
assert!(keypad.get_button_at(10, 10).is_none());
}
#[test]
fn test_keypad_find_by_label() {
let keypad = Keypad::new();
assert_eq!(keypad.find_button_by_label('7'), Some(0));
assert_eq!(keypad.find_button_by_label('0'), Some(12));
assert_eq!(keypad.find_button_by_label('='), Some(14));
assert_eq!(keypad.find_button_by_label('X'), None);
}
#[test]
fn test_keypad_find_by_char() {
let keypad = Keypad::new();
assert_eq!(keypad.find_button_by_char('5'), Some(5));
assert_eq!(keypad.find_button_by_char('+'), Some(15));
assert_eq!(keypad.find_button_by_char('.'), Some(13));
}
#[test]
fn test_keypad_press_button() {
let mut keypad = Keypad::new();
keypad.press_button(0);
assert!(keypad.get_button(0).unwrap().pressed);
assert!(!keypad.get_button(1).unwrap().pressed);
}
#[test]
fn test_keypad_release_all() {
let mut keypad = Keypad::new();
keypad.press_button(0);
keypad.press_button(5);
keypad.release_all();
for btn in keypad.buttons() {
assert!(!btn.pressed);
}
}
#[test]
fn test_keypad_highlight_char() {
let mut keypad = Keypad::new();
keypad.highlight_char('5');
assert!(keypad.get_button(5).unwrap().pressed);
assert!(!keypad.get_button(0).unwrap().pressed);
}
#[test]
fn test_keypad_buttons_iterator() {
let keypad = Keypad::new();
let count = keypad.buttons().count();
assert_eq!(count, 20);
}
#[test]
fn test_keypad_buttons_with_positions() {
let keypad = Keypad::new();
let positions: Vec<_> = keypad.buttons_with_positions().collect();
assert_eq!(positions.len(), 20);
assert_eq!(positions[0].0, (0, 0)); assert_eq!(positions[19].0, (4, 3)); }
#[test]
fn test_keypad_hit_test_inside() {
let keypad = Keypad::new();
let area = Rect::new(0, 0, 22, 12);
let result = keypad.hit_test(area, 10, 5);
assert!(result.is_some());
}
#[test]
fn test_keypad_hit_test_outside() {
let keypad = Keypad::new();
let area = Rect::new(10, 10, 22, 12);
assert!(keypad.hit_test(area, 0, 0).is_none());
assert!(keypad.hit_test(area, 100, 100).is_none());
}
#[test]
fn test_keypad_hit_test_border() {
let keypad = Keypad::new();
let area = Rect::new(0, 0, 22, 12);
assert!(keypad.hit_test(area, 0, 0).is_none());
}
#[test]
fn test_keypad_get_button_mut() {
let mut keypad = Keypad::new();
if let Some(btn) = keypad.get_button_mut(0) {
btn.set_pressed(true);
}
assert!(keypad.get_button(0).unwrap().pressed);
}
#[test]
fn test_keypad_clone() {
let keypad = Keypad::new();
let cloned = keypad.clone();
assert_eq!(keypad.button_count(), cloned.button_count());
}
#[test]
fn test_keypad_debug() {
let keypad = Keypad::new();
let debug = format!("{:?}", keypad);
assert!(debug.contains("Keypad"));
}
#[test]
fn test_keypad_row_1() {
let keypad = Keypad::new();
assert_eq!(keypad.get_button_at(0, 0).unwrap().label, '7');
assert_eq!(keypad.get_button_at(0, 1).unwrap().label, '8');
assert_eq!(keypad.get_button_at(0, 2).unwrap().label, '9');
assert_eq!(keypad.get_button_at(0, 3).unwrap().label, '/');
}
#[test]
fn test_keypad_row_2() {
let keypad = Keypad::new();
assert_eq!(keypad.get_button_at(1, 0).unwrap().label, '4');
assert_eq!(keypad.get_button_at(1, 1).unwrap().label, '5');
assert_eq!(keypad.get_button_at(1, 2).unwrap().label, '6');
assert_eq!(keypad.get_button_at(1, 3).unwrap().label, '*');
}
#[test]
fn test_keypad_row_3() {
let keypad = Keypad::new();
assert_eq!(keypad.get_button_at(2, 0).unwrap().label, '1');
assert_eq!(keypad.get_button_at(2, 1).unwrap().label, '2');
assert_eq!(keypad.get_button_at(2, 2).unwrap().label, '3');
assert_eq!(keypad.get_button_at(2, 3).unwrap().label, '-');
}
#[test]
fn test_keypad_row_4() {
let keypad = Keypad::new();
assert_eq!(keypad.get_button_at(3, 0).unwrap().label, '0');
assert_eq!(keypad.get_button_at(3, 1).unwrap().label, '.');
assert_eq!(keypad.get_button_at(3, 2).unwrap().label, '=');
assert_eq!(keypad.get_button_at(3, 3).unwrap().label, '+');
}
#[test]
fn test_keypad_row_5() {
let keypad = Keypad::new();
assert_eq!(keypad.get_button_at(4, 0).unwrap().label, 'C');
assert_eq!(keypad.get_button_at(4, 1).unwrap().label, '(');
assert_eq!(keypad.get_button_at(4, 2).unwrap().label, ')');
assert_eq!(keypad.get_button_at(4, 3).unwrap().label, '^');
}
#[test]
fn test_keypad_widget_new() {
let keypad = Keypad::new();
let widget = KeypadWidget::new(&keypad);
let _ = widget;
}
#[test]
fn test_keypad_widget_render() {
let keypad = Keypad::new();
let widget = KeypadWidget::new(&keypad);
let area = Rect::new(0, 0, 22, 12);
let mut buf = TextBuffer::empty(area);
widget.render(area, &mut buf);
let content = buf.to_string_content();
assert!(content.contains("Keypad"));
assert!(content.contains("[7]"));
assert!(content.contains("[+]"));
}
#[test]
fn test_keypad_widget_render_small() {
let keypad = Keypad::new();
let widget = KeypadWidget::new(&keypad);
let area = Rect::new(0, 0, 5, 5); let mut buf = TextBuffer::empty(area);
widget.render(area, &mut buf);
}
#[test]
fn test_keypad_widget_render_pressed() {
let mut keypad = Keypad::new();
keypad.press_button(0); let widget = KeypadWidget::new(&keypad);
let area = Rect::new(0, 0, 22, 12);
let mut buf = TextBuffer::empty(area);
widget.render(area, &mut buf);
let content = buf.to_string_content();
assert!(content.contains("[7]"));
}
#[test]
fn prop_all_digits_have_buttons() {
let keypad = Keypad::new();
for d in 0..=9 {
let ch = char::from_digit(d, 10).unwrap();
assert!(
keypad.find_button_by_char(ch).is_some(),
"Missing button for digit {d}"
);
}
}
#[test]
fn prop_all_operators_have_buttons() {
let keypad = Keypad::new();
for op in ['+', '-', '*', '/', '^'] {
assert!(
keypad.find_button_by_char(op).is_some(),
"Missing button for operator {op}"
);
}
}
#[test]
fn prop_button_char_roundtrip() {
let keypad = Keypad::new();
for btn in keypad.buttons() {
if let Some(ch) = btn.to_char() {
let found = keypad.find_button_by_char(ch);
assert!(found.is_some(), "Cannot find button for char '{ch}'");
}
}
}
#[test]
fn prop_press_release_idempotent() {
let mut keypad = Keypad::new();
keypad.press_button(5);
keypad.press_button(5); assert!(keypad.get_button(5).unwrap().pressed);
keypad.release_all();
keypad.release_all(); for btn in keypad.buttons() {
assert!(!btn.pressed);
}
}
#[test]
fn prop_highlight_releases_others() {
let mut keypad = Keypad::new();
keypad.press_button(0);
keypad.press_button(5);
keypad.press_button(10);
keypad.highlight_char('1');
let pressed_count = keypad.buttons().filter(|b| b.pressed).count();
assert_eq!(pressed_count, 1);
}
}