Skip to main content

pfx_editor/
input.rs

1use std::cell::RefCell;
2use std::rc::Rc;
3
4use egui::{Event, MouseWheelUnit, PointerButton};
5use pfx_input::device::{Key, MouseButton};
6use pfx_input::event::InputEvent;
7use pfx_input::{KeyLabels, LayoutSource};
8
9pub const POINTS_PER_LINE: f32 = 40.0;
10
11pub fn key(key: egui::Key) -> Option<Key> {
12    use egui::Key as E;
13    Some(match key {
14        E::A => Key::A,
15        E::B => Key::B,
16        E::C => Key::C,
17        E::D => Key::D,
18        E::E => Key::E,
19        E::F => Key::F,
20        E::G => Key::G,
21        E::H => Key::H,
22        E::I => Key::I,
23        E::J => Key::J,
24        E::K => Key::K,
25        E::L => Key::L,
26        E::M => Key::M,
27        E::N => Key::N,
28        E::O => Key::O,
29        E::P => Key::P,
30        E::Q => Key::Q,
31        E::R => Key::R,
32        E::S => Key::S,
33        E::T => Key::T,
34        E::U => Key::U,
35        E::V => Key::V,
36        E::W => Key::W,
37        E::X => Key::X,
38        E::Y => Key::Y,
39        E::Z => Key::Z,
40        E::Num0 => Key::Digit0,
41        E::Num1 => Key::Digit1,
42        E::Num2 => Key::Digit2,
43        E::Num3 => Key::Digit3,
44        E::Num4 => Key::Digit4,
45        E::Num5 => Key::Digit5,
46        E::Num6 => Key::Digit6,
47        E::Num7 => Key::Digit7,
48        E::Num8 => Key::Digit8,
49        E::Num9 => Key::Digit9,
50        E::F1 => Key::F1,
51        E::F2 => Key::F2,
52        E::F3 => Key::F3,
53        E::F4 => Key::F4,
54        E::F5 => Key::F5,
55        E::F6 => Key::F6,
56        E::F7 => Key::F7,
57        E::F8 => Key::F8,
58        E::F9 => Key::F9,
59        E::F10 => Key::F10,
60        E::F11 => Key::F11,
61        E::F12 => Key::F12,
62        E::ArrowUp => Key::Up,
63        E::ArrowDown => Key::Down,
64        E::ArrowLeft => Key::Left,
65        E::ArrowRight => Key::Right,
66        E::Escape => Key::Escape,
67        E::Tab => Key::Tab,
68        E::Space => Key::Space,
69        E::Enter => Key::Enter,
70        E::Backspace => Key::Backspace,
71        E::Insert => Key::Insert,
72        E::Delete => Key::Delete,
73        E::Home => Key::Home,
74        E::End => Key::End,
75        E::PageUp => Key::PageUp,
76        E::PageDown => Key::PageDown,
77        E::Minus => Key::Minus,
78        E::Equals => Key::Equal,
79        E::OpenBracket => Key::BracketLeft,
80        E::CloseBracket => Key::BracketRight,
81        E::Backslash => Key::Backslash,
82        E::Semicolon => Key::Semicolon,
83        E::Quote => Key::Quote,
84        E::Backtick => Key::Backquote,
85        E::Comma => Key::Comma,
86        E::Period => Key::Period,
87        E::Slash => Key::Slash,
88        _ => return None,
89    })
90}
91
92#[derive(Clone)]
93pub struct Layout(Rc<RefCell<Box<dyn LayoutSource>>>);
94
95impl LayoutSource for Layout {
96    fn layout(&mut self) -> u64 {
97        self.0.borrow_mut().layout()
98    }
99
100    fn label(&mut self, key: Key) -> Option<String> {
101        self.0.borrow_mut().label(key)
102    }
103}
104
105#[cfg(windows)]
106fn platform() -> Option<Box<dyn LayoutSource>> {
107    Some(Box::new(pfx_input::WindowsLayout::new()))
108}
109
110#[cfg(not(windows))]
111fn platform() -> Option<Box<dyn LayoutSource>> {
112    None
113}
114
115pub fn layout(configured: Option<Box<dyn LayoutSource>>) -> Option<Layout> {
116    configured
117        .or_else(platform)
118        .map(|source| Layout(Rc::new(RefCell::new(source))))
119}
120
121pub fn learn(labels: &mut KeyLabels, events: &[Event]) -> bool {
122    let mut learned = false;
123    let mut typing: Option<(Key, bool)> = None;
124    for event in events {
125        match event {
126            Event::Key {
127                physical_key: Some(physical),
128                pressed: true,
129                repeat: false,
130                modifiers,
131                ..
132            } => {
133                typing = key(*physical)
134                    .filter(|_| !modifiers.ctrl && !modifiers.alt && !modifiers.mac_cmd)
135                    .map(|key| (key, modifiers.shift));
136            }
137            Event::Key { .. } => typing = None,
138            Event::Text(text) => {
139                if let Some((key, shifted)) = typing.take()
140                    && text.chars().count() == 1
141                    && (!shifted || text.chars().all(char::is_alphabetic))
142                {
143                    learned |= labels.learn(key, text);
144                }
145            }
146            _ => {}
147        }
148    }
149    learned
150}
151
152pub fn button(button: PointerButton) -> Option<MouseButton> {
153    match button {
154        PointerButton::Primary => Some(MouseButton::Left),
155        PointerButton::Secondary => Some(MouseButton::Right),
156        PointerButton::Middle => Some(MouseButton::Middle),
157        PointerButton::Extra1 => Some(MouseButton::Back),
158        PointerButton::Extra2 => Some(MouseButton::Forward),
159    }
160}
161
162fn modifiers(was: egui::Modifiers, now: egui::Modifiers, out: &mut Vec<InputEvent>) {
163    for (key, before, after) in [
164        (Key::ShiftLeft, was.shift, now.shift),
165        (Key::ControlLeft, was.ctrl, now.ctrl),
166        (Key::AltLeft, was.alt, now.alt),
167    ] {
168        if before != after {
169            out.push(InputEvent::Key {
170                key,
171                pressed: after,
172            });
173        }
174    }
175}
176
177pub fn events(
178    events: &[Event],
179    pixels_per_point: f32,
180    was: egui::Modifiers,
181    now: egui::Modifiers,
182) -> Vec<InputEvent> {
183    let mut out = Vec::new();
184    modifiers(was, now, &mut out);
185    for event in events {
186        match event {
187            Event::Key {
188                key: logical,
189                physical_key,
190                pressed,
191                repeat: false,
192                ..
193            } => {
194                if let Some(key) = key(physical_key.unwrap_or(*logical)) {
195                    out.push(InputEvent::Key {
196                        key,
197                        pressed: *pressed,
198                    });
199                }
200            }
201            Event::PointerMoved(at) => out.push(InputEvent::PointerMoved {
202                window: [at.x * pixels_per_point, at.y * pixels_per_point],
203            }),
204            Event::PointerButton {
205                button: pressed_button,
206                pressed,
207                ..
208            } => {
209                if let Some(button) = button(*pressed_button) {
210                    out.push(InputEvent::MouseButton {
211                        button,
212                        pressed: *pressed,
213                    });
214                }
215            }
216            Event::MouseWheel { unit, delta, .. } => {
217                let lines = match unit {
218                    MouseWheelUnit::Point => *delta / POINTS_PER_LINE,
219                    MouseWheelUnit::Line | MouseWheelUnit::Page => *delta,
220                };
221                out.push(InputEvent::Wheel {
222                    x: lines.x,
223                    y: lines.y,
224                });
225            }
226            Event::PointerGone => out.push(InputEvent::PointerLeft),
227            Event::WindowFocused(false) => out.push(InputEvent::FocusLost),
228            _ => {}
229        }
230    }
231    out
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237
238    fn press(key: egui::Key, physical: Option<egui::Key>, pressed: bool) -> Event {
239        Event::Key {
240            key,
241            physical_key: physical,
242            pressed,
243            repeat: false,
244            modifiers: egui::Modifiers::NONE,
245        }
246    }
247
248    #[test]
249    fn keys_buttons_wheels_and_pointers_reach_the_game_in_window_pixels() {
250        let typed = [
251            press(egui::Key::Z, Some(egui::Key::Y), true),
252            press(egui::Key::F5, None, false),
253            Event::Key {
254                key: egui::Key::A,
255                physical_key: None,
256                pressed: true,
257                repeat: true,
258                modifiers: egui::Modifiers::NONE,
259            },
260            Event::PointerMoved(egui::pos2(10.0, 20.0)),
261            Event::PointerButton {
262                pos: egui::pos2(10.0, 20.0),
263                button: PointerButton::Secondary,
264                pressed: true,
265                modifiers: egui::Modifiers::NONE,
266            },
267            Event::MouseWheel {
268                unit: MouseWheelUnit::Point,
269                delta: egui::vec2(0.0, -80.0),
270                phase: egui::TouchPhase::Move,
271                modifiers: egui::Modifiers::NONE,
272            },
273            Event::PointerGone,
274            Event::WindowFocused(false),
275            Event::Text("y".into()),
276        ];
277        assert_eq!(
278            events(&typed, 2.0, egui::Modifiers::NONE, egui::Modifiers::NONE),
279            vec![
280                InputEvent::Key {
281                    key: Key::Y,
282                    pressed: true
283                },
284                InputEvent::Key {
285                    key: Key::F5,
286                    pressed: false
287                },
288                InputEvent::PointerMoved {
289                    window: [20.0, 40.0]
290                },
291                InputEvent::MouseButton {
292                    button: MouseButton::Right,
293                    pressed: true
294                },
295                InputEvent::Wheel { x: 0.0, y: -2.0 },
296                InputEvent::PointerLeft,
297                InputEvent::FocusLost,
298            ]
299        );
300    }
301
302    fn typed(physical: egui::Key, text: &str, modifiers: egui::Modifiers) -> [Event; 2] {
303        [
304            Event::Key {
305                key: physical,
306                physical_key: Some(physical),
307                pressed: true,
308                repeat: false,
309                modifiers,
310            },
311            Event::Text(text.into()),
312        ]
313    }
314
315    #[test]
316    fn typed_keys_teach_labels_by_physical_position_and_never_when_a_chord() {
317        let none = egui::Modifiers::NONE;
318        let mut labels = KeyLabels::new();
319        assert_eq!(labels.label(Key::Q), "Q");
320        assert!(learn(&mut labels, &typed(egui::Key::Q, "a", none)));
321        assert_eq!(labels.label(Key::Q), "A");
322        assert!(!learn(&mut labels, &typed(egui::Key::Q, "a", none)));
323        let shift = egui::Modifiers {
324            shift: true,
325            ..none
326        };
327        assert!(!learn(&mut labels, &typed(egui::Key::Num1, "!", shift)));
328        assert_eq!(labels.label(Key::Digit1), "1");
329        let ctrl = egui::Modifiers { ctrl: true, ..none };
330        assert!(!learn(&mut labels, &typed(egui::Key::W, "z", ctrl)));
331        assert_eq!(labels.label(Key::W), "W");
332        assert!(!learn(&mut labels, &typed(egui::Key::F5, "x", none)));
333        let held = [
334            Event::Key {
335                key: egui::Key::W,
336                physical_key: Some(egui::Key::W),
337                pressed: true,
338                repeat: true,
339                modifiers: none,
340            },
341            Event::Text("z".into()),
342        ];
343        assert!(!learn(&mut labels, &held));
344        let mut unmapped = typed(egui::Key::Z, "y", none);
345        if let Event::Key { physical_key, .. } = &mut unmapped[0] {
346            *physical_key = None;
347        }
348        assert!(!learn(&mut labels, &unmapped));
349        assert!(learn(&mut labels, &typed(egui::Key::Y, "z", shift)));
350        assert_eq!(labels.label(Key::Y), "Z");
351    }
352
353    struct Azerty;
354
355    impl LayoutSource for Azerty {
356        fn layout(&mut self) -> u64 {
357            1
358        }
359
360        fn label(&mut self, key: Key) -> Option<String> {
361            (key == Key::Q).then(|| "a".to_string())
362        }
363    }
364
365    fn driver(layout: Option<Layout>) -> pfx_game::Driver {
366        let session = pfx_play::PlaySession::play_with(
367            &pfx_load::scene::Scene::empty(),
368            None,
369            Box::new(pfx_play::SceneGame),
370            pfx_play::Options::default(),
371        )
372        .unwrap();
373        let mut driver = pfx_game::Driver::new(
374            session,
375            pfx_play::Settings::default(),
376            crate::session::free_policy(),
377            pfx_gpu::screens::Device::Desktop,
378            pfx_gpu::window::Size {
379                width: 640,
380                height: 360,
381            },
382            None,
383        );
384        driver.set_layout(layout.map(|layout| Box::new(layout) as Box<dyn LayoutSource>));
385        driver
386    }
387
388    #[test]
389    fn an_azerty_stand_in_teaches_the_play_driver_that_q_reads_a() {
390        let none = egui::Modifiers::NONE;
391        let mut played = driver(None);
392        played.update(0);
393        assert_eq!(played.labels().label(Key::Q), "Q");
394        assert!(learn(played.labels_mut(), &typed(egui::Key::Q, "a", none)));
395        played.update(16_666_667);
396        assert_eq!(played.labels().label(Key::Q), "A");
397        assert_eq!(played.labels().label(Key::W), "W");
398    }
399
400    #[test]
401    fn a_configured_layout_source_labels_the_play_driver_and_is_shared_across_plays() {
402        let layout = layout(Some(Box::new(Azerty))).unwrap();
403        for _ in 0..2 {
404            let mut played = driver(Some(layout.clone()));
405            played.update(0);
406            assert_eq!(played.labels().label(Key::Q), "A");
407        }
408    }
409
410    #[test]
411    fn held_modifiers_press_and_release_their_left_keys() {
412        let none = egui::Modifiers::NONE;
413        let shift = egui::Modifiers {
414            shift: true,
415            ..none
416        };
417        let ctrl = egui::Modifiers { ctrl: true, ..none };
418        let pressed = |key, pressed| InputEvent::Key { key, pressed };
419        assert_eq!(
420            events(&[], 1.0, none, shift),
421            vec![pressed(Key::ShiftLeft, true)]
422        );
423        assert_eq!(
424            events(&[press(egui::Key::W, None, true)], 1.0, shift, ctrl),
425            vec![
426                pressed(Key::ShiftLeft, false),
427                pressed(Key::ControlLeft, true),
428                pressed(Key::W, true),
429            ]
430        );
431        assert!(events(&[], 1.0, ctrl, ctrl).is_empty());
432    }
433}