mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-02-28 15:22:14 +00:00
feat(eckhart): keypad component
This commit is contained in:
parent
305f535067
commit
478cf9045d
@ -0,0 +1,571 @@
|
|||||||
|
use crate::{
|
||||||
|
trezorhal::random,
|
||||||
|
ui::{
|
||||||
|
component::{Component, Event, EventCtx, Maybe},
|
||||||
|
geometry::{Alignment, Insets, Offset, Rect},
|
||||||
|
shape::Renderer,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
super::super::{
|
||||||
|
component::button::{Button, ButtonContent, ButtonMsg, ButtonStyleSheet},
|
||||||
|
constant::SCREEN,
|
||||||
|
theme,
|
||||||
|
},
|
||||||
|
common::KEYPAD_VISIBLE_HEIGHT,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
pub enum KeypadButton {
|
||||||
|
Key(usize),
|
||||||
|
Erase, // Represents an erase button.
|
||||||
|
Cancel, // Represents a cancel button.
|
||||||
|
Confirm, // Represents a confirm button.
|
||||||
|
Back, // Represents a back(previous) button.
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ButtonState {
|
||||||
|
Enabled,
|
||||||
|
Disabled,
|
||||||
|
Hidden,
|
||||||
|
}
|
||||||
|
|
||||||
|
const KEYPAD_MAX_KEYS: usize = 10;
|
||||||
|
type KeypadKeys = [Maybe<Button>; KEYPAD_MAX_KEYS];
|
||||||
|
|
||||||
|
pub struct KeypadState {
|
||||||
|
pub back: ButtonState,
|
||||||
|
pub erase: ButtonState,
|
||||||
|
pub cancel: ButtonState,
|
||||||
|
pub confirm: ButtonState,
|
||||||
|
pub keys: ButtonState,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Keypad {
|
||||||
|
back: Maybe<Button>,
|
||||||
|
erase: Maybe<Button>,
|
||||||
|
cancel: Maybe<Button>,
|
||||||
|
confirm: Maybe<Button>,
|
||||||
|
keys: KeypadKeys,
|
||||||
|
pressed: Option<KeypadButton>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||||
|
#[cfg_attr(feature = "ui_debug", derive(ufmt::derive::uDebug))]
|
||||||
|
pub enum KeypadMsg {
|
||||||
|
Back,
|
||||||
|
Confirm,
|
||||||
|
EraseShort,
|
||||||
|
EraseLong,
|
||||||
|
Cancel,
|
||||||
|
Key(usize),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Keypad {
|
||||||
|
pub const MAX_KEYS: usize = KEYPAD_MAX_KEYS;
|
||||||
|
const KEYBOARD_BUTTON_HEIGHT: i16 = 70;
|
||||||
|
const KEYBOARD_BUTTON_RADIUS: u8 = 11;
|
||||||
|
|
||||||
|
const ERASE_BUTTON_INDEX: usize = 9;
|
||||||
|
const CONFIRM_BUTTON_INDEX: usize = 11;
|
||||||
|
|
||||||
|
/// Create a new keypad with numeric keys. The keys are shown and active and
|
||||||
|
/// are shuffled if `shuffle` is true. The special buttons are hidden.
|
||||||
|
pub fn new_numeric(shuffle: bool) -> Self {
|
||||||
|
let mut digits = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"];
|
||||||
|
// Shuffle if requested
|
||||||
|
if shuffle {
|
||||||
|
random::shuffle(&mut digits);
|
||||||
|
}
|
||||||
|
let keypad_content: [_; KEYPAD_MAX_KEYS] =
|
||||||
|
core::array::from_fn(|i| ButtonContent::Text(digits[i].into()));
|
||||||
|
|
||||||
|
Self::new_inner(true, true, theme::button_keyboard_numeric())
|
||||||
|
.with_keys_content(&keypad_content)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new keypad with empty keys. The keys and special buttons are
|
||||||
|
/// hidden.
|
||||||
|
pub fn new_hidden() -> Self {
|
||||||
|
Self::new_inner(false, false, theme::button_keyboard())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new keypad with empty keys. The keys are shown and the special
|
||||||
|
/// buttons are hidden.
|
||||||
|
pub fn new_shown() -> Self {
|
||||||
|
Self::new_inner(true, true, theme::button_keyboard())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_inner(enabled: bool, visible: bool, styles: ButtonStyleSheet) -> Self {
|
||||||
|
Self {
|
||||||
|
// Special buttons are hidden by default.
|
||||||
|
back: Maybe::hidden(
|
||||||
|
theme::BG,
|
||||||
|
Button::with_icon(theme::ICON_CHEVRON_LEFT)
|
||||||
|
.styled(theme::button_keyboard_numeric())
|
||||||
|
.with_radius(Self::KEYBOARD_BUTTON_RADIUS)
|
||||||
|
.initially_enabled(false),
|
||||||
|
),
|
||||||
|
cancel: Maybe::hidden(
|
||||||
|
theme::BG,
|
||||||
|
Button::with_icon(theme::ICON_CROSS)
|
||||||
|
.styled(theme::button_cancel())
|
||||||
|
.with_radius(Self::KEYBOARD_BUTTON_RADIUS)
|
||||||
|
.initially_enabled(false),
|
||||||
|
),
|
||||||
|
confirm: Maybe::hidden(
|
||||||
|
theme::BG,
|
||||||
|
Button::with_icon(theme::ICON_CHECKMARK)
|
||||||
|
.styled(theme::button_keyboard_confirm())
|
||||||
|
.with_radius(Self::KEYBOARD_BUTTON_RADIUS)
|
||||||
|
.initially_enabled(false),
|
||||||
|
),
|
||||||
|
erase: Maybe::hidden(
|
||||||
|
theme::BG,
|
||||||
|
Button::with_icon(theme::ICON_DELETE)
|
||||||
|
.styled(theme::button_keyboard())
|
||||||
|
.with_long_press(theme::ERASE_HOLD_DURATION)
|
||||||
|
.with_radius(Self::KEYBOARD_BUTTON_RADIUS)
|
||||||
|
.initially_enabled(false),
|
||||||
|
),
|
||||||
|
// Initialize all keys with empty content
|
||||||
|
keys: core::array::from_fn(|_idx| {
|
||||||
|
let inner = Button::empty()
|
||||||
|
.with_radius(Self::KEYBOARD_BUTTON_RADIUS)
|
||||||
|
.styled(styles)
|
||||||
|
.with_text_align(Alignment::Center)
|
||||||
|
.initially_enabled(enabled);
|
||||||
|
Maybe::new(theme::BG, inner, visible)
|
||||||
|
}),
|
||||||
|
pressed: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the content of all keyboard keys.
|
||||||
|
pub fn with_keys_content(mut self, keypad_content: &[ButtonContent]) -> Self {
|
||||||
|
// Make sure the content fits the keypad.
|
||||||
|
debug_assert!(keypad_content.len() <= Self::MAX_KEYS);
|
||||||
|
|
||||||
|
for (i, key_content) in keypad_content.into_iter().enumerate() {
|
||||||
|
self.keys[i].inner_mut().set_content(key_content.clone());
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the content of a key at the specified index.
|
||||||
|
pub fn get_key_content(&self, idx: usize) -> &ButtonContent {
|
||||||
|
// Make sure the index is within bounds.
|
||||||
|
debug_assert!(idx < Self::MAX_KEYS);
|
||||||
|
|
||||||
|
&self.keys[idx].inner().content()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the state of the keypad.
|
||||||
|
pub fn set_state(&mut self, state: KeypadState, ctx: &mut EventCtx) {
|
||||||
|
Self::apply_button_state(&mut self.back, &state.back, ctx);
|
||||||
|
Self::apply_button_state(&mut self.erase, &state.erase, ctx);
|
||||||
|
Self::apply_button_state(&mut self.cancel, &state.cancel, ctx);
|
||||||
|
Self::apply_button_state(&mut self.confirm, &state.confirm, ctx);
|
||||||
|
self.set_keys_state(ctx, &state.keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the content of a key at the specified index.
|
||||||
|
pub fn set_key_content(&mut self, idx: usize, content: ButtonContent) {
|
||||||
|
// Make sure the index is within bounds.
|
||||||
|
debug_assert!(idx < Self::MAX_KEYS);
|
||||||
|
|
||||||
|
self.keys[idx].inner_mut().set_content(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_button_mut(&mut self, button: &KeypadButton) -> &mut Maybe<Button> {
|
||||||
|
match button {
|
||||||
|
KeypadButton::Key(idx) => &mut self.keys[*idx],
|
||||||
|
KeypadButton::Erase => &mut self.erase,
|
||||||
|
KeypadButton::Cancel => &mut self.cancel,
|
||||||
|
KeypadButton::Confirm => &mut self.confirm,
|
||||||
|
KeypadButton::Back => &mut self.back,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_button(&self, button: &KeypadButton) -> &Maybe<Button> {
|
||||||
|
match button {
|
||||||
|
KeypadButton::Key(idx) => &self.keys[*idx],
|
||||||
|
KeypadButton::Erase => &self.erase,
|
||||||
|
KeypadButton::Cancel => &self.cancel,
|
||||||
|
KeypadButton::Confirm => &self.confirm,
|
||||||
|
KeypadButton::Back => &self.back,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the stylesheet of one keypad button.
|
||||||
|
pub fn set_button_stylesheet(&mut self, button: KeypadButton, styles: ButtonStyleSheet) {
|
||||||
|
self.get_button_mut(&button)
|
||||||
|
.inner_mut()
|
||||||
|
.set_stylesheet(styles);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_button_state(btn: &mut Maybe<Button>, state: &ButtonState, ctx: &mut EventCtx) {
|
||||||
|
match state {
|
||||||
|
ButtonState::Enabled => {
|
||||||
|
btn.show(ctx);
|
||||||
|
btn.inner_mut().enable(ctx);
|
||||||
|
}
|
||||||
|
ButtonState::Disabled => {
|
||||||
|
btn.show(ctx);
|
||||||
|
btn.inner_mut().disable(ctx);
|
||||||
|
}
|
||||||
|
ButtonState::Hidden => {
|
||||||
|
btn.hide(ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the state of all key buttons
|
||||||
|
pub fn set_keys_state(&mut self, ctx: &mut EventCtx, state: &ButtonState) {
|
||||||
|
for btn in self.keys.iter_mut() {
|
||||||
|
Self::apply_button_state(btn, state, ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the state of one keypad button.
|
||||||
|
pub fn set_button_state(
|
||||||
|
&mut self,
|
||||||
|
ctx: &mut EventCtx,
|
||||||
|
button: KeypadButton,
|
||||||
|
state: &ButtonState,
|
||||||
|
) {
|
||||||
|
Self::apply_button_state(self.get_button_mut(&button), state, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if any button is currently pressed.
|
||||||
|
pub fn pressed(&self) -> bool {
|
||||||
|
self.pressed.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_button<'s>(btn: &'s Maybe<Button>, target: &mut impl Renderer<'s>) {
|
||||||
|
if btn.is_visible() {
|
||||||
|
btn.render(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render pressed button.
|
||||||
|
fn render_pressed_button<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
// TODO: Render special shape for the edge buttons.
|
||||||
|
if let Some(button) = &self.pressed {
|
||||||
|
Self::render_button(self.get_button(button), target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert key index to grid cell index.
|
||||||
|
fn key_2_grid_cell(key: usize) -> usize {
|
||||||
|
// Make sure the key is within bounds.
|
||||||
|
debug_assert!(key < Self::MAX_KEYS);
|
||||||
|
// Key with index 9 must be mapped after the cancel button.
|
||||||
|
if key < 9 {
|
||||||
|
key
|
||||||
|
} else {
|
||||||
|
key + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for Keypad {
|
||||||
|
type Msg = KeypadMsg;
|
||||||
|
|
||||||
|
fn place(&mut self, bounds: Rect) -> Rect {
|
||||||
|
// assert precise size and position of the keypad
|
||||||
|
debug_assert_eq!(bounds.width(), SCREEN.width());
|
||||||
|
debug_assert_eq!(bounds.height(), KEYPAD_VISIBLE_HEIGHT);
|
||||||
|
debug_assert!(bounds.bottom_right() == SCREEN.bottom_right());
|
||||||
|
|
||||||
|
// Decrease touch area for buttons.
|
||||||
|
let erase_touch_inset = KeypadGrid::insets_of_cell(Self::ERASE_BUTTON_INDEX);
|
||||||
|
let confirm_touch_inset = KeypadGrid::insets_of_cell(Self::CONFIRM_BUTTON_INDEX);
|
||||||
|
|
||||||
|
self.erase
|
||||||
|
.inner_mut()
|
||||||
|
.set_expanded_touch_area(erase_touch_inset);
|
||||||
|
self.cancel
|
||||||
|
.inner_mut()
|
||||||
|
.set_expanded_touch_area(erase_touch_inset);
|
||||||
|
self.back
|
||||||
|
.inner_mut()
|
||||||
|
.set_expanded_touch_area(erase_touch_inset);
|
||||||
|
self.confirm
|
||||||
|
.inner_mut()
|
||||||
|
.set_expanded_touch_area(confirm_touch_inset);
|
||||||
|
|
||||||
|
for (i, btn) in self.keys.iter_mut().enumerate() {
|
||||||
|
btn.inner_mut()
|
||||||
|
.set_expanded_touch_area(KeypadGrid::insets_of_cell(Self::key_2_grid_cell(i)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Place buttons
|
||||||
|
let erase_area = KeypadGrid::border_of_cell(Self::ERASE_BUTTON_INDEX);
|
||||||
|
let confirm_area = KeypadGrid::border_of_cell(Self::CONFIRM_BUTTON_INDEX);
|
||||||
|
|
||||||
|
self.erase.place(erase_area);
|
||||||
|
self.cancel.place(erase_area);
|
||||||
|
self.back.place(erase_area);
|
||||||
|
self.confirm.place(confirm_area);
|
||||||
|
for (i, btn) in self.keys.iter_mut().enumerate() {
|
||||||
|
btn.place(KeypadGrid::border_of_cell(Self::key_2_grid_cell(i)));
|
||||||
|
}
|
||||||
|
bounds
|
||||||
|
}
|
||||||
|
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||||
|
let click_mapping = [
|
||||||
|
(KeypadButton::Confirm, KeypadMsg::Confirm),
|
||||||
|
(KeypadButton::Erase, KeypadMsg::EraseShort),
|
||||||
|
(KeypadButton::Cancel, KeypadMsg::Cancel),
|
||||||
|
(KeypadButton::Back, KeypadMsg::Back),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (button, msg) in click_mapping {
|
||||||
|
match self.get_button_mut(&button).event(ctx, event) {
|
||||||
|
// Detect click of all special buttons
|
||||||
|
Some(ButtonMsg::Clicked) => {
|
||||||
|
self.pressed = None;
|
||||||
|
return Some(msg);
|
||||||
|
}
|
||||||
|
// Detect long press of the erase button
|
||||||
|
Some(ButtonMsg::LongPressed) if button == KeypadButton::Erase => {
|
||||||
|
self.pressed = None;
|
||||||
|
return Some(KeypadMsg::EraseLong);
|
||||||
|
}
|
||||||
|
// Detect press of all special buttons for rendering purposes
|
||||||
|
Some(ButtonMsg::Pressed) => {
|
||||||
|
self.pressed = Some(button);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (idx, btn) in self.keys.iter_mut().enumerate() {
|
||||||
|
match btn.event(ctx, event) {
|
||||||
|
// Detect click of all key buttons
|
||||||
|
Some(ButtonMsg::Clicked) => {
|
||||||
|
self.pressed = None;
|
||||||
|
return Some(KeypadMsg::Key(idx));
|
||||||
|
}
|
||||||
|
// Detect press of all key buttons for rendering purposes
|
||||||
|
Some(ButtonMsg::Pressed) => {
|
||||||
|
self.pressed = Some(KeypadButton::Key(idx));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
// render key buttons
|
||||||
|
for btn in self.keys.iter() {
|
||||||
|
Self::render_button(btn, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
// render special buttons
|
||||||
|
Self::render_button(&self.cancel, target);
|
||||||
|
Self::render_button(&self.erase, target);
|
||||||
|
Self::render_button(&self.back, target);
|
||||||
|
Self::render_button(&self.confirm, target);
|
||||||
|
|
||||||
|
// render currently pressed button once again because of possible overlap
|
||||||
|
self.render_pressed_button(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dimensions of the grid.
|
||||||
|
const ROWS: usize = 4;
|
||||||
|
const COLS: usize = 3;
|
||||||
|
|
||||||
|
/// Represents a grid of buttons with fixed dimensions of 4x3.
|
||||||
|
/// Borders of buttons overlap each other and the input component because in
|
||||||
|
/// pressed state, they are rendered larger. To avoid touch interference between
|
||||||
|
/// buttons, negative insets are used to reduce the touch area of each button
|
||||||
|
/// not to overlap.
|
||||||
|
pub struct KeypadGrid;
|
||||||
|
|
||||||
|
impl KeypadGrid {
|
||||||
|
/// The visible area of the keypad.
|
||||||
|
const VISIBLE_AREA: Rect = Self::visible_area();
|
||||||
|
|
||||||
|
// Button dimensions.
|
||||||
|
const BUTTON_WIDTH: i16 = 136;
|
||||||
|
const BUTTON_HEIGHT: i16 = 134;
|
||||||
|
|
||||||
|
// Overlap parameters.
|
||||||
|
const VERTICAL_BUTTON_SPACING: i16 = 32;
|
||||||
|
const HORIZONTAL_BUTTON_PADDING: i16 = 7;
|
||||||
|
|
||||||
|
// Precomputed border rects and touch insets for each grid entry.
|
||||||
|
const BORDER_RECTS: [[Rect; COLS]; ROWS] = KeypadGrid::compute_border_rects();
|
||||||
|
const TOUCH_INSETS: [[Insets; COLS]; ROWS] = KeypadGrid::compute_touch_insets();
|
||||||
|
|
||||||
|
// Keypad touch area is smaller than the visible area.
|
||||||
|
// Sum input and keypad gives the
|
||||||
|
const KEYPAD_TOUCH_HEIGHT: i16 = KEYPAD_VISIBLE_HEIGHT - Self::VERTICAL_BUTTON_SPACING / 2;
|
||||||
|
|
||||||
|
const fn visible_area() -> Rect {
|
||||||
|
let (_, visible_area) = SCREEN.split_bottom(KEYPAD_VISIBLE_HEIGHT);
|
||||||
|
visible_area
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes the border rects for each grid entry.
|
||||||
|
const fn compute_border_rects() -> [[Rect; COLS]; ROWS] {
|
||||||
|
let mut rects = [[Rect::zero(); COLS]; ROWS];
|
||||||
|
let mut row = 0;
|
||||||
|
while row < ROWS {
|
||||||
|
let mut col = 0;
|
||||||
|
while col < COLS {
|
||||||
|
rects[row][col] = Rect::from_top_left_and_size(
|
||||||
|
Self::VISIBLE_AREA.top_left().ofs(Offset::new(
|
||||||
|
col as i16 * (Self::BUTTON_WIDTH - 2 * Self::HORIZONTAL_BUTTON_PADDING),
|
||||||
|
row as i16 * (Self::BUTTON_HEIGHT - Self::VERTICAL_BUTTON_SPACING),
|
||||||
|
)),
|
||||||
|
Offset::new(Self::BUTTON_WIDTH, Self::BUTTON_HEIGHT),
|
||||||
|
);
|
||||||
|
col += 1;
|
||||||
|
}
|
||||||
|
row += 1;
|
||||||
|
}
|
||||||
|
rects
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes negative touch insets for each grid entry for
|
||||||
|
/// `set_expanded_touch_area` Buttonfunction
|
||||||
|
const fn compute_touch_insets() -> [[Insets; COLS]; ROWS] {
|
||||||
|
let mut insets = [[Insets::uniform(0); COLS]; ROWS];
|
||||||
|
let mut row = 0;
|
||||||
|
while row < ROWS {
|
||||||
|
let mut col = 0;
|
||||||
|
while col < COLS {
|
||||||
|
insets[row][col] = Insets::new(
|
||||||
|
-Self::VERTICAL_BUTTON_SPACING / 2,
|
||||||
|
-Self::HORIZONTAL_BUTTON_PADDING,
|
||||||
|
-Self::VERTICAL_BUTTON_SPACING / 2,
|
||||||
|
-Self::HORIZONTAL_BUTTON_PADDING,
|
||||||
|
);
|
||||||
|
// No bottom inset for the last row
|
||||||
|
if row == ROWS - 1 {
|
||||||
|
insets[row][col].bottom = 0;
|
||||||
|
}
|
||||||
|
// no left inset for the first column
|
||||||
|
if col == 0 {
|
||||||
|
insets[row][col].left = 0;
|
||||||
|
// no right inset for the last column
|
||||||
|
} else if col == COLS - 1 {
|
||||||
|
insets[row][col].right = 0;
|
||||||
|
}
|
||||||
|
col += 1;
|
||||||
|
}
|
||||||
|
row += 1;
|
||||||
|
}
|
||||||
|
insets
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves the button border `Rect` at the specified index.
|
||||||
|
pub const fn border_of_cell(index: usize) -> Rect {
|
||||||
|
let (row, col) = Self::cell2row_col(index);
|
||||||
|
Self::border_of_row_col(row, col)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converts a cell index to a (row, col) tuple.
|
||||||
|
const fn cell2row_col(index: usize) -> (usize, usize) {
|
||||||
|
(index / COLS, index % COLS)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves the button border `Rect` for the given row and column.
|
||||||
|
pub const fn border_of_row_col(row: usize, col: usize) -> Rect {
|
||||||
|
debug_assert!(row < ROWS);
|
||||||
|
debug_assert!(col < COLS);
|
||||||
|
Self::BORDER_RECTS[row][col]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves the button touch `Insets` at the specified index.
|
||||||
|
pub const fn insets_of_cell(index: usize) -> Insets {
|
||||||
|
let (row, col) = Self::cell2row_col(index);
|
||||||
|
Self::insets_of_row_col(row, col)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves the button touch `Insets` for the given row and column.
|
||||||
|
pub const fn insets_of_row_col(row: usize, col: usize) -> Insets {
|
||||||
|
debug_assert!(row < ROWS);
|
||||||
|
debug_assert!(col < COLS);
|
||||||
|
Self::TOUCH_INSETS[row][col]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
super::{super::constant::SCREEN, common::INPUT_TOUCH_HEIGHT},
|
||||||
|
*,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_layout_constraints() {
|
||||||
|
debug_assert_eq!(
|
||||||
|
KeypadGrid::KEYPAD_TOUCH_HEIGHT + INPUT_TOUCH_HEIGHT,
|
||||||
|
SCREEN.height(),
|
||||||
|
"Keypad and input touch areas do not sum into the screen height"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
(ROWS as i16) * KeypadGrid::BUTTON_HEIGHT,
|
||||||
|
KeypadGrid::KEYPAD_TOUCH_HEIGHT
|
||||||
|
+ (ROWS as i16 - 1) * KeypadGrid::VERTICAL_BUTTON_SPACING,
|
||||||
|
"Keypad height does not match expected layout constraints"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
(COLS as i16) * KeypadGrid::BUTTON_WIDTH,
|
||||||
|
KeypadGrid::VISIBLE_AREA.width()
|
||||||
|
+ (COLS as i16 - 1) * 2 * KeypadGrid::HORIZONTAL_BUTTON_PADDING,
|
||||||
|
"Keypad width does not match expected layout constraints"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_borders_within_visible_area_by_cell() {
|
||||||
|
for index in 0..(ROWS * COLS) {
|
||||||
|
let border = KeypadGrid::border_of_cell(index);
|
||||||
|
// The border is within the visible area if the intersection is equal to the
|
||||||
|
// border.
|
||||||
|
let intersection = border.clamp(KeypadGrid::VISIBLE_AREA);
|
||||||
|
assert!(
|
||||||
|
border.width() == intersection.width() && border.height() == intersection.height(),
|
||||||
|
"Border at index {} is out of keypad visible bounds",
|
||||||
|
index
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_no_touch_overlap() {
|
||||||
|
for idx1 in 0..(ROWS * COLS) {
|
||||||
|
let touch_border_1 =
|
||||||
|
KeypadGrid::border_of_cell(idx1).outset(KeypadGrid::insets_of_cell(idx1));
|
||||||
|
|
||||||
|
for idx2 in 0..(ROWS * COLS) {
|
||||||
|
if idx1 == idx2 {
|
||||||
|
continue; // Skip comparing the same cell
|
||||||
|
}
|
||||||
|
|
||||||
|
let touch_border_2 =
|
||||||
|
KeypadGrid::border_of_cell(idx2).outset(KeypadGrid::insets_of_cell(idx2));
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
touch_border_1.clamp(touch_border_2).is_empty(),
|
||||||
|
"Touch border of cell {} overlaps the touch border of
|
||||||
|
cell {}",
|
||||||
|
idx1,
|
||||||
|
idx2,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1 +1,2 @@
|
|||||||
mod common;
|
mod common;
|
||||||
|
mod keypad;
|
||||||
|
Loading…
Reference in New Issue
Block a user