use platform_core::{Key, NamedKey};
impl Default for KeyNav {
fn default() -> Self {
Self {
vim: false,
horizontal: false,
grid: false,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum KeyNavMove {
Next,
Previous,
First,
Last,
NextRow,
PreviousRow,
Activate,
Cancel,
}
#[derive(Clone, Copy)]
pub struct KeyNav {
pub vim: bool,
pub horizontal: bool,
pub grid: bool,
}
impl KeyNav {
pub fn horizontal(mut self) -> Self {
self.horizontal = true;
self
}
pub fn grid(mut self) -> Self {
self.horizontal = true;
self.grid = true;
self
}
pub fn interpret(self, key: &Key) -> Option<KeyNavMove> {
let (forward, back) = if self.horizontal {
(NamedKey::ArrowRight, NamedKey::ArrowLeft)
} else {
(NamedKey::ArrowDown, NamedKey::ArrowUp)
};
if let Key::Named(named) = key {
if *named == forward {
return Some(KeyNavMove::Next);
}
if *named == back {
return Some(KeyNavMove::Previous);
}
if self.grid {
if *named == NamedKey::ArrowDown {
return Some(KeyNavMove::NextRow);
}
if *named == NamedKey::ArrowUp {
return Some(KeyNavMove::PreviousRow);
}
}
return match named {
NamedKey::Enter => Some(KeyNavMove::Activate),
NamedKey::Escape => Some(KeyNavMove::Cancel),
NamedKey::Home => Some(KeyNavMove::First),
NamedKey::End => Some(KeyNavMove::Last),
_ => None,
};
}
if !self.vim {
return None;
}
let (down, up) = if self.grid {
(KeyNavMove::NextRow, KeyNavMove::PreviousRow)
} else {
(KeyNavMove::Next, KeyNavMove::Previous)
};
match key {
Key::Char(c) => match c {
'j' => Some(down),
'k' => Some(up),
'h' if self.grid => Some(KeyNavMove::Previous),
'l' if self.grid => Some(KeyNavMove::Next),
'g' => Some(KeyNavMove::First),
'G' => Some(KeyNavMove::Last),
'\u{e}' => Some(down), '\u{10}' => Some(up), _ => None,
},
_ => None,
}
}
}
pub fn key_nav_apply(current: usize, count: usize, movement: KeyNavMove) -> usize {
key_nav_apply_grid(current, count, 1, movement)
}
pub fn key_nav_apply_grid(
current: usize,
count: usize,
columns: usize,
movement: KeyNavMove,
) -> usize {
if count == 0 {
return 0;
}
let columns = columns.max(1);
let current = current.min(count - 1);
let last = count - 1;
match movement {
KeyNavMove::Next => (current + 1) % count,
KeyNavMove::Previous => (current + count - 1) % count,
KeyNavMove::First => 0,
KeyNavMove::Last => last,
KeyNavMove::NextRow => {
let below = current + columns;
if below <= last {
below
} else if current / columns < last / columns {
last
} else {
current % columns
}
}
KeyNavMove::PreviousRow => {
if current >= columns {
current - columns
} else {
let bottom = (last / columns) * columns + current % columns;
if bottom > last {
bottom - columns
} else {
bottom
}
}
}
KeyNavMove::Activate | KeyNavMove::Cancel => current,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn arrows() -> KeyNav {
KeyNav {
vim: false,
horizontal: false,
grid: false,
}
}
fn vim() -> KeyNav {
KeyNav {
vim: true,
horizontal: false,
grid: false,
}
}
fn named(key: NamedKey) -> Key {
Key::Named(key)
}
fn character(c: char) -> Key {
Key::Char(c)
}
#[test]
fn the_arrows_always_navigate_and_the_letters_only_do_in_vim_mode() {
assert_eq!(
arrows().interpret(&named(NamedKey::ArrowDown)),
Some(KeyNavMove::Next)
);
assert_eq!(
arrows().interpret(&named(NamedKey::ArrowUp)),
Some(KeyNavMove::Previous)
);
assert_eq!(
arrows().interpret(&named(NamedKey::Enter)),
Some(KeyNavMove::Activate)
);
assert_eq!(
arrows().interpret(&named(NamedKey::Escape)),
Some(KeyNavMove::Cancel)
);
assert_eq!(arrows().interpret(&character('j')), None);
assert_eq!(arrows().interpret(&character('k')), None);
assert_eq!(arrows().interpret(&character('G')), None);
assert_eq!(vim().interpret(&character('j')), Some(KeyNavMove::Next));
assert_eq!(vim().interpret(&character('k')), Some(KeyNavMove::Previous));
assert_eq!(vim().interpret(&character('g')), Some(KeyNavMove::First));
assert_eq!(vim().interpret(&character('G')), Some(KeyNavMove::Last));
assert_eq!(
vim().interpret(&character('\u{e}')),
Some(KeyNavMove::Next),
"Ctrl-N"
);
assert_eq!(
vim().interpret(&character('\u{10}')),
Some(KeyNavMove::Previous),
"Ctrl-P"
);
assert_eq!(
vim().interpret(&character('q')),
None,
"an unbound letter is still typing"
);
}
#[test]
fn a_horizontal_list_reads_the_other_pair_of_arrows() {
let row = arrows().horizontal();
assert_eq!(
row.interpret(&named(NamedKey::ArrowRight)),
Some(KeyNavMove::Next)
);
assert_eq!(
row.interpret(&named(NamedKey::ArrowLeft)),
Some(KeyNavMove::Previous)
);
assert_eq!(
row.interpret(&named(NamedKey::ArrowDown)),
None,
"down is not along a row, so it stays available to whatever else wants it"
);
}
#[test]
fn the_selection_wraps_at_both_ends_and_survives_a_list_that_shrank() {
assert_eq!(key_nav_apply(0, 3, KeyNavMove::Next), 1);
assert_eq!(
key_nav_apply(2, 3, KeyNavMove::Next),
0,
"wraps past the end"
);
assert_eq!(
key_nav_apply(0, 3, KeyNavMove::Previous),
2,
"and back past the start"
);
assert_eq!(key_nav_apply(1, 3, KeyNavMove::First), 0);
assert_eq!(key_nav_apply(1, 3, KeyNavMove::Last), 2);
assert_eq!(
key_nav_apply(1, 3, KeyNavMove::Activate),
1,
"activating moves nothing"
);
assert_eq!(
key_nav_apply(0, 0, KeyNavMove::Next),
0,
"an empty list has nowhere to go"
);
assert_eq!(key_nav_apply(9, 3, KeyNavMove::Next), 0);
assert_eq!(key_nav_apply(9, 3, KeyNavMove::Previous), 1);
assert_eq!(key_nav_apply(0, 3, KeyNavMove::NextRow), 1);
assert_eq!(key_nav_apply(2, 3, KeyNavMove::PreviousRow), 1);
assert_eq!(key_nav_apply(2, 3, KeyNavMove::NextRow), 0, "still wraps");
}
#[test]
fn a_grid_uses_both_pairs_of_arrows() {
let grid = arrows().grid();
assert_eq!(
grid.interpret(&named(NamedKey::ArrowRight)),
Some(KeyNavMove::Next)
);
assert_eq!(
grid.interpret(&named(NamedKey::ArrowLeft)),
Some(KeyNavMove::Previous)
);
assert_eq!(
grid.interpret(&named(NamedKey::ArrowDown)),
Some(KeyNavMove::NextRow)
);
assert_eq!(
grid.interpret(&named(NamedKey::ArrowUp)),
Some(KeyNavMove::PreviousRow)
);
assert_eq!(
grid.interpret(&named(NamedKey::Enter)),
Some(KeyNavMove::Activate)
);
assert_eq!(grid.interpret(&character('j')), None);
assert_eq!(grid.interpret(&character('h')), None);
let keys = vim().grid();
assert_eq!(keys.interpret(&character('j')), Some(KeyNavMove::NextRow));
assert_eq!(
keys.interpret(&character('k')),
Some(KeyNavMove::PreviousRow)
);
assert_eq!(keys.interpret(&character('l')), Some(KeyNavMove::Next));
assert_eq!(keys.interpret(&character('h')), Some(KeyNavMove::Previous));
}
#[test]
fn a_row_move_crosses_a_whole_row_and_a_partial_one_is_still_a_row() {
assert_eq!(key_nav_apply_grid(0, 9, 3, KeyNavMove::NextRow), 3);
assert_eq!(key_nav_apply_grid(4, 9, 3, KeyNavMove::PreviousRow), 1);
assert_eq!(
key_nav_apply_grid(1, 9, 3, KeyNavMove::Next),
2,
"along the row, not down it"
);
assert_eq!(
key_nav_apply_grid(7, 9, 3, KeyNavMove::NextRow),
1,
"wraps to the same column"
);
assert_eq!(
key_nav_apply_grid(1, 9, 3, KeyNavMove::PreviousRow),
7,
"and back to the bottom of it"
);
assert_eq!(key_nav_apply_grid(2, 5, 3, KeyNavMove::NextRow), 4);
assert_eq!(key_nav_apply_grid(1, 5, 3, KeyNavMove::NextRow), 4);
assert_eq!(key_nav_apply_grid(0, 5, 3, KeyNavMove::NextRow), 3);
assert_eq!(key_nav_apply_grid(2, 5, 3, KeyNavMove::PreviousRow), 2);
assert_eq!(key_nav_apply_grid(1, 5, 3, KeyNavMove::PreviousRow), 4);
assert_eq!(
key_nav_apply_grid(4, 5, 3, KeyNavMove::NextRow),
1,
"the last row wraps to the first"
);
assert_eq!(key_nav_apply_grid(1, 3, 3, KeyNavMove::NextRow), 1);
assert_eq!(key_nav_apply_grid(1, 3, 3, KeyNavMove::PreviousRow), 1);
assert_eq!(key_nav_apply_grid(0, 0, 4, KeyNavMove::NextRow), 0);
assert_eq!(
key_nav_apply_grid(9, 5, 3, KeyNavMove::NextRow),
1,
"a stale index is clamped first"
);
}
}