use std::collections::BTreeMap;
use bevy::input::keyboard::{Key, KeyboardInput};
use bevy::prelude::*;
use crate::app::binds;
use crate::app::i18n::Lang;
use crate::app::settings::GameSettings;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Layout {
Qwerty,
Azerty,
Qwertz,
}
impl crate::app::cycle::Cycle for Option<Layout> {
const VARIANTS: &'static [Self] = &[
None,
Some(Layout::Qwerty),
Some(Layout::Azerty),
Some(Layout::Qwertz),
];
}
impl Layout {
pub const ALL: [Layout; 3] = [Layout::Qwerty, Layout::Azerty, Layout::Qwertz];
pub fn name(self) -> &'static str {
match self {
Layout::Qwerty => "QWERTY",
Layout::Azerty => "AZERTY",
Layout::Qwertz => "QWERTZ",
}
}
pub fn key(self) -> &'static str {
match self {
Layout::Qwerty => "qwerty",
Layout::Azerty => "azerty",
Layout::Qwertz => "qwertz",
}
}
pub fn from_key(key: &str) -> Option<Layout> {
Layout::ALL.into_iter().find(|layout| layout.key() == key)
}
pub fn of(lang: Lang) -> Option<Layout> {
match lang {
Lang::Fr => Some(Layout::Azerty),
Lang::De => Some(Layout::Qwertz),
Lang::En | Lang::Es | Lang::It | Lang::Nl | Lang::Ru | Lang::Ja => None,
}
}
pub fn caps(self) -> &'static [(KeyCode, char)] {
match self {
Layout::Qwerty => &[],
Layout::Azerty => &[
(KeyCode::Backslash, '*'),
(KeyCode::BracketLeft, '^'),
(KeyCode::BracketRight, '$'),
(KeyCode::Comma, ';'),
(KeyCode::KeyA, 'Q'),
(KeyCode::KeyQ, 'A'),
(KeyCode::KeyW, 'Z'),
(KeyCode::KeyZ, 'W'),
(KeyCode::Minus, ')'),
(KeyCode::Period, ':'),
(KeyCode::Semicolon, 'M'),
(KeyCode::Slash, '!'),
],
Layout::Qwertz => &[
(KeyCode::Backquote, '^'),
(KeyCode::Backslash, '#'),
(KeyCode::BracketRight, '+'),
(KeyCode::KeyY, 'Z'),
(KeyCode::KeyZ, 'Y'),
(KeyCode::Slash, '-'),
],
}
}
pub fn tells(self) -> &'static [(KeyCode, char)] {
match self {
Layout::Qwerty => &[],
Layout::Azerty => &[
(KeyCode::KeyA, 'Q'),
(KeyCode::KeyQ, 'A'),
(KeyCode::KeyW, 'Z'),
(KeyCode::KeyZ, 'W'),
(KeyCode::Semicolon, 'M'),
],
Layout::Qwertz => &[(KeyCode::KeyY, 'Z'), (KeyCode::KeyZ, 'Y')],
}
}
fn cap(self, key: KeyCode) -> Option<char> {
self.caps().iter().find(|(k, _)| *k == key).map(|(_, c)| *c)
}
fn key_for(self, letter: char) -> Option<KeyCode> {
self.caps()
.iter()
.find(|(_, c)| *c == letter)
.map(|(k, _)| *k)
}
}
#[derive(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]),
];
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) -> KeyCode {
let letter = letter.to_ascii_uppercase();
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),
)
.or_else(|| binds::key_from_name(&format!("Key{letter}")))
.or_else(|| binds::global_key_from_name(&format!("Key{letter}")))
.unwrap_or_else(|| panic!("no key spells {letter}"))
}
pub fn just_pressed(&self, keys: &ButtonInput<KeyCode>, letter: char) -> bool {
keys.just_pressed(self.key_for(letter))
}
pub fn is_global(&self, key: KeyCode) -> bool {
binds::GLOBAL_LETTERS
.iter()
.any(|&l| self.key_for(l) == 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() || !learnable(key) {
return false;
}
let cap = cap.to_ascii_uppercase();
let before = self.clone();
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);
} else {
self.learned.insert(key, cap);
}
for layout in Layout::ALL {
if layout.tells().contains(&(key, cap)) {
for &(k, c) in layout.caps() {
self.learned.entry(k).or_insert(c);
}
}
}
*self != before
}
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();
if let (Some(key), Some(c), None) =
(binds::key_from_name(name), chars.next(), chars.next())
&& learnable(key)
&& c.is_ascii_graphic()
{
caps.learned.insert(key, c.to_ascii_uppercase());
}
}
caps
}
}
fn learnable(key: KeyCode) -> bool {
let name = binds::key_name(key);
binds::bindable(key)
&& key != KeyCode::Space
&& !name.starts_with("Digit")
&& !name.starts_with("Numpad")
}
pub fn learn_keycaps(
mut typed: MessageReader<KeyboardInput>,
keys: Res<ButtonInput<KeyCode>>,
screen: Res<State<crate::app::Screen>>,
mut settings: ResMut<GameSettings>,
) {
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 caps = settings.keycaps.clone();
if caps.learn(input.key_code, cap) {
settings.keycaps = caps;
changed = true;
}
}
if changed && crate::app::language::may_save(screen.get()) {
settings.save();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_layout_table_is_learnable_and_says_something_new() {
for layout in Layout::ALL {
let mut seen = Vec::new();
for &(key, cap) in layout.caps() {
assert!(cap.is_ascii_graphic(), "{layout:?} {key:?}");
assert!(learnable(key), "{layout:?} {key:?}");
assert_ne!(binds::key_label(key), cap.to_string(), "{layout:?}");
assert!(!seen.contains(&key), "{layout:?} spells {key:?} twice");
seen.push(key);
}
for tell in layout.tells() {
assert!(layout.caps().contains(tell), "{layout:?} {tell:?}");
}
}
}
#[test]
fn every_language_names_its_keyboard() {
assert_eq!(Layout::of(Lang::Fr), Some(Layout::Azerty));
assert_eq!(Layout::of(Lang::De), Some(Layout::Qwertz));
for lang in crate::app::i18n::ALL_LANGS {
if matches!(lang, Lang::Fr | Lang::De) {
continue;
}
assert_eq!(Layout::of(lang), None, "{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'), 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'), 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'), 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'));
}
#[test]
fn mnemonics_follow_the_cap() {
let caps = KeyCaps::default();
assert_eq!(caps.key_for('M'), KeyCode::KeyM);
assert_eq!(caps.key_for('h'), 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'), KeyCode::Semicolon);
assert_eq!(azerty.key_for('A'), KeyCode::KeyQ);
assert_eq!(azerty.key_for('H'), KeyCode::KeyH);
assert!(azerty.is_global(KeyCode::Semicolon));
assert!(!binds::bindable(KeyCode::KeyM));
let mut keys = ButtonInput::<KeyCode>::default();
keys.press(KeyCode::Semicolon);
assert!(azerty.just_pressed(&keys, 'M'));
assert!(!caps.just_pressed(&keys, 'M'));
}
#[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 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 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::hud::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_match_setup] {
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'),
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());
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'), 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'), 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'), 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 \
KeyQ=A KeyW=Z KeyZ=W Minus=) Period=: 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);
}
}