1use egui::Key;
2
3pub trait Code: Copy {
4 fn code(self) -> &'static str;
5}
6
7pub fn from_code<P: Code>(panels: &[P], code: &str) -> Option<P> {
8 panels
9 .iter()
10 .copied()
11 .find(|panel| panel.code().eq_ignore_ascii_case(code))
12}
13
14fn letter(key: Key) -> Option<char> {
15 let name = key.name();
16 let mut chars = name.chars();
17 match (chars.next(), chars.next()) {
18 (Some(letter), None) if letter.is_ascii_alphabetic() => Some(letter.to_ascii_lowercase()),
19 _ => None,
20 }
21}
22
23fn digit(key: Key) -> Option<char> {
24 match key {
25 Key::Num0 => Some('0'),
26 Key::Num1 => Some('1'),
27 Key::Num2 => Some('2'),
28 Key::Num3 => Some('3'),
29 Key::Num4 => Some('4'),
30 Key::Num5 => Some('5'),
31 Key::Num6 => Some('6'),
32 Key::Num7 => Some('7'),
33 Key::Num8 => Some('8'),
34 Key::Num9 => Some('9'),
35 _ => None,
36 }
37}
38
39fn starts<P: Code>(panels: &[P], letter: char) -> bool {
40 panels.iter().any(|panel| {
41 panel
42 .code()
43 .chars()
44 .next()
45 .is_some_and(|first| first.eq_ignore_ascii_case(&letter))
46 })
47}
48
49fn second<P: Code>(panels: &[P], digit: char) -> bool {
50 panels
51 .iter()
52 .any(|panel| panel.code().chars().nth(1) == Some(digit))
53}
54
55#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
56pub struct Keys {
57 pending: Option<char>,
58}
59
60#[derive(Clone, Copy, Debug, PartialEq, Eq)]
61pub enum Press<P> {
62 Focus(P),
63 Pending,
64 Other(Key),
65}
66
67impl Keys {
68 pub fn pending(&self) -> Option<char> {
69 self.pending
70 }
71
72 pub fn press<P: Code>(&mut self, panels: &[P], key: Key) -> Press<P> {
73 let pending = self.pending.take();
74 if let Some(letter) = letter(key).filter(|letter| starts(panels, *letter)) {
75 return match from_code(panels, &letter.to_string()) {
76 Some(panel) => Press::Focus(panel),
77 None => {
78 self.pending = Some(letter);
79 Press::Pending
80 }
81 };
82 }
83 let digit = digit(key).filter(|digit| second(panels, *digit));
84 if let (Some(letter), Some(digit)) = (pending, digit)
85 && let Some(panel) = from_code(panels, &format!("{letter}{digit}"))
86 {
87 return Press::Focus(panel);
88 }
89 if pending.is_some() && digit.is_some() {
90 return Press::Pending;
91 }
92 Press::Other(key)
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99
100 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
101 enum Panel {
102 A,
103 B1,
104 B2,
105 B3,
106 C1,
107 C2,
108 C3,
109 D1,
110 }
111
112 const ALL: [Panel; 8] = [
113 Panel::A,
114 Panel::B1,
115 Panel::B2,
116 Panel::B3,
117 Panel::C1,
118 Panel::C2,
119 Panel::C3,
120 Panel::D1,
121 ];
122
123 impl Code for Panel {
124 fn code(self) -> &'static str {
125 match self {
126 Panel::A => "A",
127 Panel::B1 => "B1",
128 Panel::B2 => "B2",
129 Panel::B3 => "B3",
130 Panel::C1 => "C1",
131 Panel::C2 => "C2",
132 Panel::C3 => "C3",
133 Panel::D1 => "D1",
134 }
135 }
136 }
137
138 fn run(keys: &[Key]) -> Vec<Press<Panel>> {
139 let mut state = Keys::default();
140 keys.iter().map(|key| state.press(&ALL, *key)).collect()
141 }
142
143 #[test]
144 fn a_letter_then_a_digit_jumps() {
145 assert_eq!(
146 run(&[Key::B, Key::Num1]),
147 vec![Press::Pending, Press::Focus(Panel::B1)]
148 );
149 assert_eq!(
150 run(&[Key::C, Key::Num3]),
151 vec![Press::Pending, Press::Focus(Panel::C3)]
152 );
153 assert_eq!(
154 run(&[Key::D, Key::Num1]),
155 vec![Press::Pending, Press::Focus(Panel::D1)]
156 );
157 }
158
159 #[test]
160 fn the_top_bar_answers_its_letter() {
161 assert_eq!(run(&[Key::A]), vec![Press::Focus(Panel::A)]);
162 }
163
164 #[test]
165 fn a_broken_sequence_forgets_its_letter() {
166 assert_eq!(
167 run(&[Key::B, Key::X, Key::Num1]),
168 vec![
169 Press::Pending,
170 Press::Other(Key::X),
171 Press::Other(Key::Num1)
172 ]
173 );
174 assert_eq!(
175 run(&[Key::D, Key::Num2, Key::Num1]),
176 vec![Press::Pending, Press::Pending, Press::Other(Key::Num1)]
177 );
178 assert_eq!(run(&[Key::Num1]), vec![Press::Other(Key::Num1)]);
179 }
180
181 #[test]
182 fn letters_and_digits_that_no_code_uses_pass_through() {
183 assert_eq!(
184 run(&[Key::E, Key::B, Key::Num4, Key::Num1]),
185 vec![
186 Press::Other(Key::E),
187 Press::Pending,
188 Press::Other(Key::Num4),
189 Press::Other(Key::Num1)
190 ]
191 );
192 assert_eq!(run(&[Key::Num0]), vec![Press::Other(Key::Num0)]);
193 }
194
195 #[test]
196 fn every_code_names_its_panel_in_either_case() {
197 for panel in ALL {
198 assert_eq!(from_code(&ALL, panel.code()), Some(panel));
199 assert_eq!(
200 from_code(&ALL, &panel.code().to_ascii_lowercase()),
201 Some(panel)
202 );
203 }
204 assert_eq!(from_code(&ALL, "E1"), None);
205 }
206
207 #[test]
208 fn other_codes_parse_their_own_chords() {
209 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
210 struct Named(&'static str);
211
212 impl Code for Named {
213 fn code(self) -> &'static str {
214 self.0
215 }
216 }
217
218 let panels = [Named("X"), Named("Y7"), Named("Y9")];
219 let mut keys = Keys::default();
220 assert_eq!(keys.press(&panels, Key::X), Press::Focus(Named("X")));
221 assert_eq!(keys.press(&panels, Key::Y), Press::Pending);
222 assert_eq!(keys.pending(), Some('y'));
223 assert_eq!(keys.press(&panels, Key::Num9), Press::Focus(Named("Y9")));
224 assert_eq!(keys.press(&panels, Key::A), Press::Other(Key::A));
225 }
226}