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}