Skip to main content

pfx_editor_shell/
focus.rs

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}