mod layout;
pub use layout::Layout;
use std::collections::BTreeMap;
use bevy::input::keyboard::{Key, KeyboardInput};
use bevy::prelude::*;
use crate::app::binds;
use crate::app::cursor::LiveSeatKeys;
use crate::app::settings::GameSettings;
#[derive(Resource, Clone, PartialEq, Eq, Debug, Default)]
pub struct KeyCaps {
learned: BTreeMap<KeyCode, char>,
presumed: Option<Layout>,
forced: Option<Layout>,
}
const BLOCKS: &[(&str, &[KeyCode])] = &[
(
"WASD",
&[KeyCode::KeyW, KeyCode::KeyA, KeyCode::KeyS, KeyCode::KeyD],
),
(
"IJKL",
&[KeyCode::KeyI, KeyCode::KeyJ, KeyCode::KeyK, KeyCode::KeyL],
),
("W/S", &[KeyCode::KeyW, KeyCode::KeyS]),
("A/D", &[KeyCode::KeyA, KeyCode::KeyD]),
];
#[derive(bevy::ecs::system::SystemParam)]
pub struct Keyboard<'w> {
pub keys: Res<'w, ButtonInput<KeyCode>>,
pub caps: Res<'w, KeyCaps>,
pub live: Res<'w, LiveSeatKeys>,
}
impl Keyboard<'_> {
pub fn global(&self, letter: char) -> bool {
self.caps.global_pressed(&self.keys, letter, &self.live)
}
pub fn just(&self, letter: char) -> bool {
self.caps.just_pressed(&self.keys, letter)
}
}
#[derive(bevy::ecs::system::SystemParam)]
pub struct KeyboardMut<'w> {
pub keys: ResMut<'w, ButtonInput<KeyCode>>,
pub caps: Res<'w, KeyCaps>,
pub live: Res<'w, LiveSeatKeys>,
}
impl KeyboardMut<'_> {
pub fn global(&self, letter: char) -> bool {
self.caps.global_pressed(&self.keys, letter, &self.live)
}
pub fn just(&self, letter: char) -> bool {
self.caps.just_pressed(&self.keys, letter)
}
}
impl KeyCaps {
pub fn cap(&self, key: KeyCode) -> Option<char> {
if let Some(forced) = self.forced {
return forced.cap(key);
}
self.learned
.get(&key)
.copied()
.or_else(|| self.presumed.and_then(|layout| layout.cap(key)))
}
pub fn label(&self, key: KeyCode) -> String {
self.cap(key)
.map_or_else(|| binds::key_label(key), |c| c.to_string())
}
pub fn key_for(&self, letter: char) -> Option<KeyCode> {
let letter = letter.to_ascii_uppercase();
let named = self.forced.map_or_else(
|| {
self.learned
.iter()
.find(|(_, c)| **c == letter)
.map(|(k, _)| *k)
.or_else(|| self.presumed.and_then(|layout| layout.key_for(letter)))
},
|forced| forced.key_for(letter),
);
named.or_else(|| {
let name = format!("Key{letter}");
binds::any_key_from_name(&name)
.filter(|&key| self.cap(key).is_none_or(|cap| cap == letter))
})
}
pub fn just_pressed(&self, keys: &ButtonInput<KeyCode>, letter: char) -> bool {
debug_assert!(
!binds::GLOBAL_LETTERS.contains(&letter.to_ascii_uppercase()),
"{letter} is global: read it through Globals"
);
self.key_for(letter)
.is_some_and(|key| keys.just_pressed(key))
}
pub fn global_pressed(
&self,
keys: &ButtonInput<KeyCode>,
letter: char,
live: &LiveSeatKeys,
) -> bool {
self.key_for(letter)
.is_some_and(|key| keys.just_pressed(key) && !live.holds(key))
}
pub fn pressed_caps<'a>(
&'a self,
keys: &'a ButtonInput<KeyCode>,
cap: char,
) -> impl Iterator<Item = KeyCode> + 'a {
keys.get_just_pressed()
.copied()
.filter(move |&key| self.cap(key) == Some(cap) || self.digit_sign(key) == Some(cap))
}
fn digit_sign(&self, key: KeyCode) -> Option<char> {
Layout::ALL
.into_iter()
.filter(|layout| {
layout
.tells()
.iter()
.all(|&(tell, said)| self.cap(tell) == Some(said))
})
.flat_map(Layout::digit_signs)
.find(|(k, _)| *k == key)
.map(|(_, sign)| *sign)
}
pub fn is_global(&self, key: KeyCode) -> bool {
binds::GLOBAL_LETTERS
.iter()
.any(|&l| self.key_for(l) == Some(key))
}
pub fn legend(&self, text: &str) -> String {
let mut out = text.to_string();
for (spelling, keys) in BLOCKS {
if !out.contains(spelling) {
continue;
}
let mut keys = keys.iter();
let caps: String = spelling
.chars()
.map(|c| {
if c.is_ascii_alphabetic() {
keys.next().map_or_else(String::new, |&key| self.label(key))
} else {
c.to_string()
}
})
.collect();
out = out.replace(spelling, &caps);
}
out
}
pub fn adopt(&mut self, keymap: &[(KeyCode, char)]) -> bool {
let mut changed = false;
for &(key, cap) in keymap {
changed |= self.learn(key, cap);
}
changed
}
pub fn force(&mut self, layout: Option<Layout>) {
self.forced = layout;
}
pub fn presume(&mut self, layout: Option<Layout>) {
self.presumed = layout.filter(|layout| {
layout
.caps()
.iter()
.all(|&(key, cap)| self.learned.get(&key).is_none_or(|&said| said == cap))
});
}
pub fn learn(&mut self, key: KeyCode, cap: char) -> bool {
if !cap.is_ascii_graphic() {
return false;
}
let cap = cap.to_ascii_uppercase();
let before = self.clone();
if is_numpad(key) {
return false;
}
if !learnable(key) {
self.evict(key, cap);
return *self != before;
}
let disproves = self
.presumed
.and_then(|layout| layout.cap(key))
.is_some_and(|presumed| presumed != cap);
if disproves {
self.presumed = None;
}
if !disproves && binds::key_label(key) == cap.to_string() {
self.learned.remove(&key);
self.evict(key, cap);
} else {
self.claim(key, cap);
}
for layout in Layout::ALL {
if layout.tells().contains(&(key, cap)) {
for &(k, c) in layout.caps() {
if !self.learned.contains_key(&k) {
self.claim(k, c);
}
}
}
}
*self != before
}
fn claim(&mut self, key: KeyCode, cap: char) {
self.evict(key, cap);
self.learned.insert(key, cap);
}
fn evict(&mut self, key: KeyCode, cap: char) {
self.learned.retain(|&k, &mut c| k == key || c != cap);
}
pub fn to_text(&self) -> String {
self.learned
.iter()
.map(|(k, c)| format!("{}={c}", binds::key_name(*k)))
.collect::<Vec<_>>()
.join(" ")
}
pub fn parse(text: &str) -> KeyCaps {
let mut caps = KeyCaps::default();
for token in text.split_whitespace() {
let Some((name, cap)) = token.split_once('=') else {
continue;
};
let mut chars = cap.chars();
let key = binds::any_key_from_name(name);
if let (Some(key), Some(c), None) = (key, chars.next(), chars.next())
&& learnable(key)
&& c.is_ascii_graphic()
{
caps.learned.insert(key, c.to_ascii_uppercase());
}
}
caps
}
}
pub(super) fn learnable(key: KeyCode) -> bool {
let name = binds::key_name(key);
(binds::bindable(key) || binds::GLOBAL_KEYS.contains(&key))
&& key != KeyCode::Space
&& !name.starts_with("Digit")
&& !is_numpad(key)
}
fn is_numpad(key: KeyCode) -> bool {
binds::key_name(key).starts_with("Numpad")
}
pub fn learn_keycaps(
mut typed: MessageReader<KeyboardInput>,
keys: Res<ButtonInput<KeyCode>>,
unasked: Res<crate::app::language::LanguageUnasked>,
settings: Res<GameSettings>,
mut caps: ResMut<KeyCaps>,
) {
const MODIFIERS: [KeyCode; 8] = [
KeyCode::ShiftLeft,
KeyCode::ShiftRight,
KeyCode::ControlLeft,
KeyCode::ControlRight,
KeyCode::AltLeft,
KeyCode::AltRight,
KeyCode::SuperLeft,
KeyCode::SuperRight,
];
let mut changed = false;
for input in typed.read() {
if !input.state.is_pressed() || input.repeat || keys.any_pressed(MODIFIERS) {
continue;
}
let Key::Character(text) = &input.logical_key else {
continue;
};
let mut chars = text.chars();
let (Some(cap), None) = (chars.next(), chars.next()) else {
continue;
};
let mut learned = caps.clone();
if learned.learn(input.key_code, cap) {
*caps = learned;
changed = true;
}
}
if changed && unasked.may_save() {
settings.save(&caps);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::app::i18n::Lang;
#[test]
fn the_language_presumes_its_countrys_keyboard() {
let mut caps = KeyCaps::default();
caps.presume(Layout::of(Lang::Fr));
assert_eq!(caps.legend("WASD bouger | IJKL"), "ZQSD bouger | IJKL");
assert_eq!(caps.key_for('M'), Some(KeyCode::Semicolon));
assert!(caps.is_global(KeyCode::Semicolon));
assert_eq!(caps.to_text(), "");
let mut german = KeyCaps::default();
german.presume(Layout::of(Lang::De));
assert_eq!(german.label(KeyCode::KeyY), "Z");
assert_eq!(german.label(KeyCode::Slash), "-");
assert_eq!(
german.legend("WASD"),
"WASD",
"QWERTZ leaves the block alone"
);
assert_eq!(german.key_for('M'), Some(KeyCode::KeyM));
let mut english = KeyCaps::default();
english.presume(Layout::of(Lang::En));
assert_eq!(english, KeyCaps::default());
}
#[test]
fn one_press_disproves_the_languages_keyboard() {
let mut caps = KeyCaps::default();
caps.presume(Layout::of(Lang::Fr));
assert!(caps.learn(KeyCode::KeyW, 'w'));
assert_eq!(caps.presumed, None);
assert_eq!(caps.label(KeyCode::KeyW), "W");
assert_eq!(
caps.key_for('M'),
Some(KeyCode::KeyM),
"the mnemonics come home"
);
assert_eq!(caps.legend("WASD"), "WASD");
let reloaded = KeyCaps::parse(&caps.to_text());
assert_eq!(reloaded.to_text(), "KeyW=W");
let mut reloaded = reloaded;
reloaded.presume(Layout::of(Lang::Fr));
assert_eq!(reloaded.presumed, None);
assert_eq!(reloaded.legend("WASD"), "WASD");
}
#[test]
fn a_learned_board_refuses_the_languages_guess() {
let mut caps = KeyCaps::default();
caps.learn(KeyCode::KeyZ, 'y');
caps.presume(Layout::of(Lang::Fr));
assert_eq!(caps.presumed, None);
assert_eq!(caps.label(KeyCode::KeyY), "Z");
assert_eq!(caps.legend("WASD"), "WASD");
}
#[test]
fn a_qwerty_board_learns_nothing() {
let mut caps = KeyCaps::default();
assert!(!caps.learn(KeyCode::KeyW, 'w'));
assert!(!caps.learn(KeyCode::Comma, ','));
assert_eq!(caps, KeyCaps::default());
assert_eq!(caps.legend("WASD move | IJKL"), "WASD move | IJKL");
assert_eq!(caps.label(KeyCode::KeyW), "W");
assert_eq!(caps.to_text(), "");
}
#[test]
fn azerty_is_learned_from_one_press_and_respells_the_legends() {
let mut caps = KeyCaps::default();
assert!(caps.learn(KeyCode::KeyW, 'z'));
assert_eq!(caps.label(KeyCode::KeyW), "Z");
assert_eq!(caps.label(KeyCode::KeyA), "Q");
assert_eq!(caps.label(KeyCode::Semicolon), "M");
assert_eq!(caps.label(KeyCode::Slash), "!");
assert_eq!(
caps.legend("WASD bouger | flèches placer | IJKL"),
"ZQSD bouger | flèches placer | IJKL"
);
assert!(!caps.learn(KeyCode::KeyW, 'z'));
}
fn learned_from(key: KeyCode, text: &str, held: Option<KeyCode>, repeat: bool) -> String {
use bevy::input::ButtonState;
let mut app = App::new();
app.add_plugins(bevy::state::app::StatesPlugin);
app.init_state::<crate::app::Screen>();
app.insert_resource(State::new(crate::app::Screen::Settings));
app.insert_resource(GameSettings::default());
app.init_resource::<crate::app::language::LanguageUnasked>();
app.init_resource::<KeyCaps>();
app.init_resource::<ButtonInput<KeyCode>>();
app.add_message::<KeyboardInput>();
app.add_systems(Update, learn_keycaps);
if let Some(held) = held {
app.world_mut()
.resource_mut::<ButtonInput<KeyCode>>()
.press(held);
}
app.world_mut().write_message(KeyboardInput {
key_code: key,
logical_key: Key::Character(text.into()),
state: ButtonState::Pressed,
text: Some(text.into()),
repeat,
window: Entity::PLACEHOLDER,
});
app.update();
app.world().resource::<KeyCaps>().label(key)
}
#[test]
fn only_a_plain_press_of_one_character_teaches_a_cap() {
assert_eq!(learned_from(KeyCode::KeyW, "z", None, false), "Z");
assert_eq!(
learned_from(KeyCode::KeyW, "z", None, true),
"W",
"a repeat"
);
assert_eq!(
learned_from(KeyCode::KeyW, "Z", Some(KeyCode::ShiftLeft), false),
"W",
"under Shift"
);
assert_eq!(
learned_from(KeyCode::KeyE, "€", Some(KeyCode::AltRight), false),
"E",
"under AltGr"
);
assert_eq!(
learned_from(KeyCode::KeyW, "zz", None, false),
"W",
"two characters"
);
}
#[test]
fn mnemonics_follow_the_cap() {
let caps = KeyCaps::default();
assert_eq!(caps.key_for('M'), Some(KeyCode::KeyM));
assert_eq!(caps.key_for('h'), Some(KeyCode::KeyH));
assert!(caps.is_global(KeyCode::KeyM));
assert!(!caps.is_global(KeyCode::Semicolon));
let mut azerty = KeyCaps::default();
azerty.learn(KeyCode::KeyW, 'z');
assert_eq!(azerty.key_for('M'), Some(KeyCode::Semicolon));
assert_eq!(azerty.key_for('A'), Some(KeyCode::KeyQ));
assert_eq!(azerty.key_for('H'), Some(KeyCode::KeyH));
assert!(azerty.is_global(KeyCode::Semicolon));
assert!(!binds::bindable(KeyCode::KeyM));
let mut keys = ButtonInput::<KeyCode>::default();
keys.press(KeyCode::Semicolon);
let none = LiveSeatKeys::default();
assert!(azerty.global_pressed(&keys, 'M', &none));
assert!(!caps.global_pressed(&keys, 'M', &none));
}
#[test]
fn a_seat_key_that_is_a_global_cap_here_stays_the_seats() {
use crate::app::Screen;
use binds::Action;
let mut settings = GameSettings::default();
settings.binds[0].set(Action::ClearAll, KeyCode::Semicolon);
let mut azerty = KeyCaps::default();
azerty.presume(Some(Layout::Azerty));
assert_eq!(azerty.key_for('M'), Some(KeyCode::Semicolon));
let live = |screen| LiveSeatKeys::of(&settings, screen, false, []);
let mut keys = ButtonInput::<KeyCode>::default();
keys.press(KeyCode::Semicolon);
assert!(!azerty.global_pressed(&keys, 'M', &live(Screen::Puzzle)));
assert!(!azerty.global_pressed(&keys, 'M', &live(Screen::Versus)));
assert!(azerty.global_pressed(&keys, 'M', &live(Screen::Menu)));
let stock = LiveSeatKeys::of(&GameSettings::default(), Screen::Versus, false, []);
assert!(azerty.global_pressed(&keys, 'M', &stock));
settings.commit = crate::app::settings::CommitScheme::Ijkl;
let mut dvorak = KeyCaps::default();
dvorak.learn(KeyCode::KeyJ, 'h');
let mut keys = ButtonInput::<KeyCode>::default();
keys.press(KeyCode::KeyJ);
let puzzle = LiveSeatKeys::of(&settings, Screen::Puzzle, false, []);
assert!(!dvorak.global_pressed(&keys, 'H', &puzzle));
}
#[test]
fn a_coop_partners_keys_are_live_on_a_puzzle() {
use crate::app::Screen;
use crate::app::cursor::Cursor;
let mut settings = GameSettings::default();
settings.commit = crate::app::settings::CommitScheme::Ijkl;
let mut dvorak = KeyCaps::default();
dvorak.learn(KeyCode::KeyL, 'n');
assert_eq!(dvorak.key_for('N'), Some(KeyCode::KeyL));
let mut keys = ButtonInput::<KeyCode>::default();
keys.press(KeyCode::KeyL);
let pair = [Cursor::seated(0), Cursor::seated(1)];
let coop = LiveSeatKeys::of(&settings, Screen::Puzzle, false, &pair);
assert!(!dvorak.global_pressed(&keys, 'N', &coop));
assert!(coop.holds(KeyCode::ArrowUp), "P1 commits on the arrows");
let solo = LiveSeatKeys::of(&settings, Screen::Puzzle, false, &pair[..1]);
assert!(!dvorak.global_pressed(&keys, 'N', &solo));
assert!(!solo.holds(KeyCode::ArrowUp));
settings.commit = crate::app::settings::CommitScheme::Arrows;
let solo = LiveSeatKeys::of(&settings, Screen::Puzzle, false, &pair[..1]);
assert!(dvorak.global_pressed(&keys, 'N', &solo));
}
fn letter_board(rows: [&str; 3]) -> Vec<(KeyCode, char)> {
let qwerty = ["qwertyuiop", "asdfghjkl;", "zxcvbnm,./"];
let mut board = Vec::new();
for (spelled, typed) in qwerty.iter().zip(rows) {
for (q, c) in spelled.chars().zip(typed.chars()) {
let name = match q {
';' => "Semicolon".to_string(),
',' => "Comma".to_string(),
'.' => "Period".to_string(),
'/' => "Slash".to_string(),
q => format!("Key{}", q.to_ascii_uppercase()),
};
let key = binds::any_key_from_name(&name).unwrap();
board.push((key, c));
}
}
board
}
#[test]
fn global_letters_are_found_by_cap_on_dvorak_and_colemak() {
use binds::GLOBAL_LETTERS;
let dvorak = letter_board(["',.pyfgcrl", "aoeuidhtns", ";qjkxbmwvz"]);
let colemak = letter_board(["qwfpgjluy;", "arstdhneio", "zxcvbkm,./"]);
for (name, board) in [("dvorak", &dvorak), ("colemak", &colemak)] {
let mut caps = KeyCaps::default();
caps.adopt(board);
for &(key, cap) in board.iter() {
let cap = cap.to_ascii_uppercase();
if cap.is_ascii_alphabetic() {
assert_eq!(caps.key_for(cap), Some(key), "{name}: {cap}");
}
let global = GLOBAL_LETTERS.contains(&cap);
assert_eq!(caps.is_global(key), global, "{name}: {key:?}");
}
assert_eq!(caps.key_for('P'), Some(KeyCode::KeyR), "{name}");
let mut keys = ButtonInput::<KeyCode>::default();
keys.press(KeyCode::KeyP);
let none = LiveSeatKeys::default();
assert!(!caps.global_pressed(&keys, 'P', &none), "{name}");
keys.press(KeyCode::KeyR);
assert!(caps.global_pressed(&keys, 'P', &none), "{name}");
assert_eq!(KeyCaps::parse(&caps.to_text()), caps, "{name}");
let mut pressed = KeyCaps::default();
pressed.learn(KeyCode::KeyR, 'p');
assert_eq!(pressed.key_for('P'), Some(KeyCode::KeyR), "{name}");
assert!(!pressed.is_global(KeyCode::KeyP), "{name}");
}
let mut partial = KeyCaps::default();
partial.learn(KeyCode::KeyP, 'l');
assert_eq!(partial.key_for('P'), None);
assert_eq!(partial.key_for('L'), Some(KeyCode::KeyP));
let mut keys = ButtonInput::<KeyCode>::default();
keys.press(KeyCode::KeyP);
assert!(!partial.global_pressed(&keys, 'P', &LiveSeatKeys::default()));
for (key, typed) in [(KeyCode::KeyR, 'r'), (KeyCode::KeyP, 'p')] {
let mut caps = KeyCaps::default();
caps.adopt(&dvorak);
caps.learn(key, typed);
assert_eq!(caps.key_for(typed), Some(key));
}
}
#[test]
fn qwertz_swaps_y_and_z() {
let mut caps = KeyCaps::default();
assert!(caps.learn(KeyCode::KeyZ, 'y'));
assert_eq!(caps.label(KeyCode::KeyY), "Z");
assert_eq!(caps.label(KeyCode::KeyZ), "Y");
assert_eq!(caps.legend("WASD"), "WASD");
}
#[test]
fn a_press_overrides_a_presumption_and_can_unlearn() {
let mut caps = KeyCaps::default();
caps.learn(KeyCode::KeyW, 'z');
assert!(caps.learn(KeyCode::Semicolon, ';'));
assert_eq!(caps.label(KeyCode::Semicolon), ";");
assert!(caps.learn(KeyCode::KeyW, 'w'));
assert_eq!(caps.label(KeyCode::KeyW), "W");
}
#[test]
fn a_cap_learned_on_one_key_leaves_the_key_that_had_it() {
let mut caps = KeyCaps::default();
caps.learn(KeyCode::KeyL, 'n');
assert_eq!(caps.key_for('N'), Some(KeyCode::KeyL));
assert!(caps.learn(KeyCode::KeyN, 'n'));
assert_eq!(caps.key_for('N'), Some(KeyCode::KeyN));
assert_eq!(caps.label(KeyCode::KeyL), "L");
let mut caps = KeyCaps::default();
caps.learn(KeyCode::KeyZ, 'y');
assert_eq!(caps.key_for('Z'), Some(KeyCode::KeyY));
caps.learn(KeyCode::KeyZ, 'z');
assert_eq!(caps.key_for('Z'), Some(KeyCode::KeyZ));
let mut caps = KeyCaps::default();
caps.learn(KeyCode::KeyL, 'n');
caps.learn(KeyCode::KeyB, 'n');
assert_eq!(caps.key_for('N'), Some(KeyCode::KeyB));
assert_eq!(caps.to_text(), "KeyB=N");
}
#[test]
fn digits_and_non_ascii_are_ignored() {
let mut caps = KeyCaps::default();
assert!(!caps.learn(KeyCode::Digit1, '&'));
assert!(!caps.learn(KeyCode::Numpad1, '1'));
assert!(!caps.learn(KeyCode::KeyW, 'ц'));
assert!(!caps.learn(KeyCode::KeyE, '€'));
assert!(!caps.learn(KeyCode::Space, ' '));
assert_eq!(caps, KeyCaps::default());
}
#[test]
fn a_numpad_press_leaves_the_main_blocks_caps_alone() {
let mut qwertz = KeyCaps::default();
qwertz.learn(KeyCode::KeyZ, 'y');
assert!(!qwertz.learn(KeyCode::NumpadSubtract, '-'));
assert!(!qwertz.learn(KeyCode::NumpadAdd, '+'));
assert_eq!(qwertz.label(KeyCode::Slash), "-");
assert_eq!(qwertz.label(KeyCode::BracketRight), "+");
let mut azerty = KeyCaps::default();
azerty.learn(KeyCode::KeyW, 'z');
assert!(!azerty.learn(KeyCode::NumpadMultiply, '*'));
assert!(!azerty.learn(KeyCode::NumpadDivide, '/'));
assert_eq!(azerty.label(KeyCode::Backslash), "*");
let mut switched = KeyCaps::default();
switched.learn(KeyCode::KeyZ, 'y');
assert!(switched.learn(KeyCode::Digit6, '-'));
assert_eq!(switched.label(KeyCode::Slash), "/");
}
#[test]
fn every_language_names_the_blocks_by_their_qwerty_caps() {
let mut azerty = KeyCaps::default();
azerty.learn(KeyCode::KeyW, 'z');
for lang in crate::app::i18n::ALL_LANGS {
let tr = lang.tr();
for legend in [
tr.prompt_setup,
tr.prompt_versus_short,
tr.prompt_versus_local,
tr.ed_prompt,
lang.level_hint(crate::app::campaign::KEY_LESSON_LEVEL)
.unwrap(),
] {
assert!(legend.contains("WASD"), "{lang:?}: {legend:?}");
let respelled = azerty.legend(legend);
assert!(respelled.contains("ZQSD"), "{lang:?}: {respelled:?}");
assert!(!respelled.contains("WASD"), "{lang:?}: {respelled:?}");
}
assert!(tr.prompt_versus_local.contains("IJKL"), "{lang:?}");
assert!(tr.val_ijkl.contains("IJKL"), "{lang:?}");
}
}
#[test]
fn every_block_spells_as_many_letters_as_it_names_keys() {
for (spelling, keys) in BLOCKS {
let letters = spelling.chars().filter(char::is_ascii_alphabetic).count();
assert_eq!(letters, keys.len(), "{spelling}");
}
let mut lengths: Vec<usize> = BLOCKS.iter().map(|(s, _)| s.len()).collect();
let sorted = {
let mut sorted = lengths.clone();
sorted.sort_unstable_by(|a, b| b.cmp(a));
sorted
};
lengths.dedup();
assert_eq!(
BLOCKS.iter().map(|(s, _)| s.len()).collect::<Vec<_>>(),
sorted,
"the blocks are not longest-first"
);
}
#[test]
fn every_language_names_the_menu_keys_by_their_qwerty_caps() {
let mut azerty = KeyCaps::default();
azerty.presume(Some(Layout::Azerty));
for lang in crate::app::i18n::ALL_LANGS {
let tr = lang.tr();
for prompt in [
tr.menu_prompt,
tr.prompt_new_version,
tr.prompt_pick_language,
tr.prompt_replays,
tr.prompt_controls,
] {
assert!(prompt.contains("W/S"), "{lang:?}: {prompt:?}");
let respelled = azerty.legend(prompt);
assert!(respelled.contains("Z/S"), "{lang:?}: {respelled:?}");
assert!(!respelled.contains("W/S"), "{lang:?}: {respelled:?}");
}
for prompt in [tr.prompt_settings, tr.prompt_setup_game] {
assert!(prompt.contains("A/D"), "{lang:?}: {prompt:?}");
let respelled = azerty.legend(prompt);
assert!(respelled.contains("Q/D"), "{lang:?}: {respelled:?}");
assert!(!respelled.contains("A/D"), "{lang:?}: {respelled:?}");
}
}
}
#[test]
fn a_keymap_read_from_the_platform_moves_everything() {
let french = [
(KeyCode::KeyA, 'q'),
(KeyCode::KeyM, ','),
(KeyCode::KeyQ, 'a'),
(KeyCode::KeyW, 'z'),
(KeyCode::KeyZ, 'w'),
(KeyCode::Comma, ';'),
(KeyCode::Period, ':'),
(KeyCode::Slash, '!'),
(KeyCode::Semicolon, 'm'),
(KeyCode::Backslash, '*'),
(KeyCode::Minus, ')'),
(KeyCode::Equal, '='),
];
let mut caps = KeyCaps::default();
assert!(caps.adopt(&french));
assert_eq!(caps.legend("WASD move | IJKL"), "ZQSD move | IJKL");
assert_eq!(
caps.legend("W/S: choose | A/D: adjust"),
"Z/S: choose | Q/D: adjust"
);
assert_eq!(
caps.key_for('M'),
Some(KeyCode::Semicolon),
"mute moves to its cap"
);
assert!(caps.is_global(KeyCode::Semicolon));
assert_eq!(caps.label(KeyCode::Slash), "!");
assert!(!caps.to_text().contains("Equal"), "{}", caps.to_text());
assert!(caps.to_text().contains("KeyM=,"), "{}", caps.to_text());
assert!(!caps.is_global(KeyCode::KeyM));
let us: Vec<(KeyCode, char)> = french
.iter()
.map(|&(key, _)| (key, binds::key_label(key).chars().next().unwrap()))
.collect();
assert!(caps.adopt(&us));
assert_eq!(
caps.legend("WASD move | W/S: choose"),
"WASD move | W/S: choose"
);
assert_eq!(caps.key_for('M'), Some(KeyCode::KeyM));
}
#[test]
fn a_named_keyboard_outranks_what_the_game_worked_out() {
let mut caps = KeyCaps::default();
caps.presume(Some(Layout::Azerty));
caps.adopt(&[(KeyCode::KeyW, 'z'), (KeyCode::Semicolon, 'm')]);
assert_eq!(caps.legend("WASD | W/S"), "ZQSD | Z/S");
caps.force(Some(Layout::Qwerty));
assert_eq!(caps.legend("WASD | W/S"), "WASD | W/S");
assert_eq!(caps.label(KeyCode::Semicolon), ";");
assert_eq!(caps.key_for('M'), Some(KeyCode::KeyM));
assert!(!caps.is_global(KeyCode::Semicolon));
let mut fresh = KeyCaps::default();
fresh.force(Some(Layout::Qwertz));
assert_eq!(fresh.label(KeyCode::KeyY), "Z");
assert_eq!(fresh.legend("WASD"), "WASD");
caps.force(None);
assert_eq!(caps.legend("WASD | W/S"), "ZQSD | Z/S");
assert_eq!(caps.key_for('M'), Some(KeyCode::Semicolon));
}
#[test]
fn text_round_trips_and_parses_leniently() {
let mut caps = KeyCaps::default();
caps.learn(KeyCode::KeyW, 'z');
caps.learn(KeyCode::Slash, ':');
let text = caps.to_text();
assert_eq!(KeyCaps::parse(&text), caps);
assert_eq!(
text,
"Backslash=* BracketLeft=^ BracketRight=$ Comma=; KeyA=Q \
KeyM=, KeyQ=A KeyW=Z KeyZ=W Minus=) Semicolon=M Slash=:"
);
let lenient = KeyCaps::parse("nonsense KeyW=Z NoSuchKey=Q KeyA=QQ Digit1=& KeyE=");
let mut expected = KeyCaps::default();
expected.learned.insert(KeyCode::KeyW, 'Z');
assert_eq!(lenient, expected);
}
}