Skip to main content

khora_infra/platform/
input.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Adapter layer: translates winit window events into Khora's abstract input events.
16//!
17//! The abstract types (`InputEvent`, `KeyCode`, `MouseButton`) live in
18//! `khora_core::platform::input`. This module only provides the
19//! `winit`-specific translation function plus the 1-to-1 winit→Khora
20//! `KeyCode` table.
21
22use std::sync::OnceLock;
23
24use winit::event::{ElementState, MouseButton as WinitMouseButton, MouseScrollDelta, WindowEvent};
25use winit::keyboard::{KeyCode as WinitKeyCode, PhysicalKey};
26
27use khora_core::platform::{InputEvent, KeyCode, MouseButton};
28
29/// Translates a `winit::event::WindowEvent` into Khora's `InputEvent` format.
30///
31/// Returns `Some(InputEvent)` for recognized input actions, `None` for window
32/// lifecycle events (resize, focus, etc.) the engine doesn't care about.
33pub fn translate_winit_input(event: &WindowEvent) -> Option<InputEvent> {
34    match event {
35        WindowEvent::KeyboardInput {
36            event: key_event, ..
37        } => {
38            if let PhysicalKey::Code(keycode) = key_event.physical_key {
39                let key_code = map_keycode(keycode);
40                match key_event.state {
41                    ElementState::Pressed if !key_event.repeat => {
42                        Some(InputEvent::KeyPressed { key_code })
43                    }
44                    ElementState::Released => Some(InputEvent::KeyReleased { key_code }),
45                    _ => None,
46                }
47            } else {
48                None
49            }
50        }
51        WindowEvent::CursorMoved { position, .. } => Some(InputEvent::MouseMoved {
52            x: position.x as f32,
53            y: position.y as f32,
54        }),
55        WindowEvent::MouseInput { state, button, .. } => {
56            let khora_button = map_mouse_button(*button);
57            match state {
58                ElementState::Pressed => Some(InputEvent::MouseButtonPressed {
59                    button: khora_button,
60                }),
61                ElementState::Released => Some(InputEvent::MouseButtonReleased {
62                    button: khora_button,
63                }),
64            }
65        }
66        WindowEvent::MouseWheel { delta, .. } => {
67            let (dx, dy): (f32, f32) = match delta {
68                MouseScrollDelta::LineDelta(x, y) => (*x, *y),
69                MouseScrollDelta::PixelDelta(pos) => (pos.x as f32, pos.y as f32),
70            };
71            if dx != 0.0 || dy != 0.0 {
72                Some(InputEvent::MouseWheelScrolled {
73                    delta_x: dx,
74                    delta_y: dy,
75                })
76            } else {
77                None
78            }
79        }
80        _ => None,
81    }
82}
83
84// --- Private Helper Functions ---
85
86/// 1-to-1 mapping from `winit::keyboard::KeyCode` to Khora's typed
87/// [`KeyCode`]. Names line up exactly with the W3C UI Events `code`
88/// attribute on both sides. Anything we miss (winit gains a new variant,
89/// platform reports something exotic) lands on
90/// [`KeyCode::Unidentified`] with a one-shot warning so the gap is
91/// observable.
92pub(crate) fn map_keycode(keycode: WinitKeyCode) -> KeyCode {
93    match keycode {
94        WinitKeyCode::Backquote => KeyCode::Backquote,
95        WinitKeyCode::Backslash => KeyCode::Backslash,
96        WinitKeyCode::BracketLeft => KeyCode::BracketLeft,
97        WinitKeyCode::BracketRight => KeyCode::BracketRight,
98        WinitKeyCode::Comma => KeyCode::Comma,
99        WinitKeyCode::Digit0 => KeyCode::Digit0,
100        WinitKeyCode::Digit1 => KeyCode::Digit1,
101        WinitKeyCode::Digit2 => KeyCode::Digit2,
102        WinitKeyCode::Digit3 => KeyCode::Digit3,
103        WinitKeyCode::Digit4 => KeyCode::Digit4,
104        WinitKeyCode::Digit5 => KeyCode::Digit5,
105        WinitKeyCode::Digit6 => KeyCode::Digit6,
106        WinitKeyCode::Digit7 => KeyCode::Digit7,
107        WinitKeyCode::Digit8 => KeyCode::Digit8,
108        WinitKeyCode::Digit9 => KeyCode::Digit9,
109        WinitKeyCode::Equal => KeyCode::Equal,
110        WinitKeyCode::IntlBackslash => KeyCode::IntlBackslash,
111        WinitKeyCode::IntlRo => KeyCode::IntlRo,
112        WinitKeyCode::IntlYen => KeyCode::IntlYen,
113        WinitKeyCode::KeyA => KeyCode::KeyA,
114        WinitKeyCode::KeyB => KeyCode::KeyB,
115        WinitKeyCode::KeyC => KeyCode::KeyC,
116        WinitKeyCode::KeyD => KeyCode::KeyD,
117        WinitKeyCode::KeyE => KeyCode::KeyE,
118        WinitKeyCode::KeyF => KeyCode::KeyF,
119        WinitKeyCode::KeyG => KeyCode::KeyG,
120        WinitKeyCode::KeyH => KeyCode::KeyH,
121        WinitKeyCode::KeyI => KeyCode::KeyI,
122        WinitKeyCode::KeyJ => KeyCode::KeyJ,
123        WinitKeyCode::KeyK => KeyCode::KeyK,
124        WinitKeyCode::KeyL => KeyCode::KeyL,
125        WinitKeyCode::KeyM => KeyCode::KeyM,
126        WinitKeyCode::KeyN => KeyCode::KeyN,
127        WinitKeyCode::KeyO => KeyCode::KeyO,
128        WinitKeyCode::KeyP => KeyCode::KeyP,
129        WinitKeyCode::KeyQ => KeyCode::KeyQ,
130        WinitKeyCode::KeyR => KeyCode::KeyR,
131        WinitKeyCode::KeyS => KeyCode::KeyS,
132        WinitKeyCode::KeyT => KeyCode::KeyT,
133        WinitKeyCode::KeyU => KeyCode::KeyU,
134        WinitKeyCode::KeyV => KeyCode::KeyV,
135        WinitKeyCode::KeyW => KeyCode::KeyW,
136        WinitKeyCode::KeyX => KeyCode::KeyX,
137        WinitKeyCode::KeyY => KeyCode::KeyY,
138        WinitKeyCode::KeyZ => KeyCode::KeyZ,
139        WinitKeyCode::Minus => KeyCode::Minus,
140        WinitKeyCode::Period => KeyCode::Period,
141        WinitKeyCode::Quote => KeyCode::Quote,
142        WinitKeyCode::Semicolon => KeyCode::Semicolon,
143        WinitKeyCode::Slash => KeyCode::Slash,
144        WinitKeyCode::AltLeft => KeyCode::AltLeft,
145        WinitKeyCode::AltRight => KeyCode::AltRight,
146        WinitKeyCode::Backspace => KeyCode::Backspace,
147        WinitKeyCode::CapsLock => KeyCode::CapsLock,
148        WinitKeyCode::ContextMenu => KeyCode::ContextMenu,
149        WinitKeyCode::ControlLeft => KeyCode::ControlLeft,
150        WinitKeyCode::ControlRight => KeyCode::ControlRight,
151        WinitKeyCode::Enter => KeyCode::Enter,
152        WinitKeyCode::SuperLeft => KeyCode::SuperLeft,
153        WinitKeyCode::SuperRight => KeyCode::SuperRight,
154        WinitKeyCode::ShiftLeft => KeyCode::ShiftLeft,
155        WinitKeyCode::ShiftRight => KeyCode::ShiftRight,
156        WinitKeyCode::Space => KeyCode::Space,
157        WinitKeyCode::Tab => KeyCode::Tab,
158        WinitKeyCode::Convert => KeyCode::Convert,
159        WinitKeyCode::KanaMode => KeyCode::KanaMode,
160        WinitKeyCode::Lang1 => KeyCode::Lang1,
161        WinitKeyCode::Lang2 => KeyCode::Lang2,
162        WinitKeyCode::Lang3 => KeyCode::Lang3,
163        WinitKeyCode::Lang4 => KeyCode::Lang4,
164        WinitKeyCode::Lang5 => KeyCode::Lang5,
165        WinitKeyCode::NonConvert => KeyCode::NonConvert,
166        WinitKeyCode::Delete => KeyCode::Delete,
167        WinitKeyCode::End => KeyCode::End,
168        WinitKeyCode::Help => KeyCode::Help,
169        WinitKeyCode::Home => KeyCode::Home,
170        WinitKeyCode::Insert => KeyCode::Insert,
171        WinitKeyCode::PageDown => KeyCode::PageDown,
172        WinitKeyCode::PageUp => KeyCode::PageUp,
173        WinitKeyCode::ArrowDown => KeyCode::ArrowDown,
174        WinitKeyCode::ArrowLeft => KeyCode::ArrowLeft,
175        WinitKeyCode::ArrowRight => KeyCode::ArrowRight,
176        WinitKeyCode::ArrowUp => KeyCode::ArrowUp,
177        WinitKeyCode::NumLock => KeyCode::NumLock,
178        WinitKeyCode::Numpad0 => KeyCode::Numpad0,
179        WinitKeyCode::Numpad1 => KeyCode::Numpad1,
180        WinitKeyCode::Numpad2 => KeyCode::Numpad2,
181        WinitKeyCode::Numpad3 => KeyCode::Numpad3,
182        WinitKeyCode::Numpad4 => KeyCode::Numpad4,
183        WinitKeyCode::Numpad5 => KeyCode::Numpad5,
184        WinitKeyCode::Numpad6 => KeyCode::Numpad6,
185        WinitKeyCode::Numpad7 => KeyCode::Numpad7,
186        WinitKeyCode::Numpad8 => KeyCode::Numpad8,
187        WinitKeyCode::Numpad9 => KeyCode::Numpad9,
188        WinitKeyCode::NumpadAdd => KeyCode::NumpadAdd,
189        WinitKeyCode::NumpadBackspace => KeyCode::NumpadBackspace,
190        WinitKeyCode::NumpadClear => KeyCode::NumpadClear,
191        WinitKeyCode::NumpadClearEntry => KeyCode::NumpadClearEntry,
192        WinitKeyCode::NumpadComma => KeyCode::NumpadComma,
193        WinitKeyCode::NumpadDecimal => KeyCode::NumpadDecimal,
194        WinitKeyCode::NumpadDivide => KeyCode::NumpadDivide,
195        WinitKeyCode::NumpadEnter => KeyCode::NumpadEnter,
196        WinitKeyCode::NumpadEqual => KeyCode::NumpadEqual,
197        WinitKeyCode::NumpadHash => KeyCode::NumpadHash,
198        WinitKeyCode::NumpadMemoryAdd => KeyCode::NumpadMemoryAdd,
199        WinitKeyCode::NumpadMemoryClear => KeyCode::NumpadMemoryClear,
200        WinitKeyCode::NumpadMemoryRecall => KeyCode::NumpadMemoryRecall,
201        WinitKeyCode::NumpadMemoryStore => KeyCode::NumpadMemoryStore,
202        WinitKeyCode::NumpadMemorySubtract => KeyCode::NumpadMemorySubtract,
203        WinitKeyCode::NumpadMultiply => KeyCode::NumpadMultiply,
204        WinitKeyCode::NumpadParenLeft => KeyCode::NumpadParenLeft,
205        WinitKeyCode::NumpadParenRight => KeyCode::NumpadParenRight,
206        WinitKeyCode::NumpadStar => KeyCode::NumpadStar,
207        WinitKeyCode::NumpadSubtract => KeyCode::NumpadSubtract,
208        WinitKeyCode::Escape => KeyCode::Escape,
209        WinitKeyCode::Fn => KeyCode::Fn,
210        WinitKeyCode::FnLock => KeyCode::FnLock,
211        WinitKeyCode::PrintScreen => KeyCode::PrintScreen,
212        WinitKeyCode::ScrollLock => KeyCode::ScrollLock,
213        WinitKeyCode::Pause => KeyCode::Pause,
214        WinitKeyCode::BrowserBack => KeyCode::BrowserBack,
215        WinitKeyCode::BrowserFavorites => KeyCode::BrowserFavorites,
216        WinitKeyCode::BrowserForward => KeyCode::BrowserForward,
217        WinitKeyCode::BrowserHome => KeyCode::BrowserHome,
218        WinitKeyCode::BrowserRefresh => KeyCode::BrowserRefresh,
219        WinitKeyCode::BrowserSearch => KeyCode::BrowserSearch,
220        WinitKeyCode::BrowserStop => KeyCode::BrowserStop,
221        WinitKeyCode::Eject => KeyCode::Eject,
222        WinitKeyCode::LaunchApp1 => KeyCode::LaunchApp1,
223        WinitKeyCode::LaunchApp2 => KeyCode::LaunchApp2,
224        WinitKeyCode::LaunchMail => KeyCode::LaunchMail,
225        WinitKeyCode::MediaPlayPause => KeyCode::MediaPlayPause,
226        WinitKeyCode::MediaSelect => KeyCode::MediaSelect,
227        WinitKeyCode::MediaStop => KeyCode::MediaStop,
228        WinitKeyCode::MediaTrackNext => KeyCode::MediaTrackNext,
229        WinitKeyCode::MediaTrackPrevious => KeyCode::MediaTrackPrevious,
230        WinitKeyCode::Power => KeyCode::Power,
231        WinitKeyCode::Sleep => KeyCode::Sleep,
232        WinitKeyCode::AudioVolumeDown => KeyCode::AudioVolumeDown,
233        WinitKeyCode::AudioVolumeMute => KeyCode::AudioVolumeMute,
234        WinitKeyCode::AudioVolumeUp => KeyCode::AudioVolumeUp,
235        WinitKeyCode::WakeUp => KeyCode::WakeUp,
236        WinitKeyCode::Meta => KeyCode::Meta,
237        WinitKeyCode::Hyper => KeyCode::Hyper,
238        WinitKeyCode::Turbo => KeyCode::Turbo,
239        WinitKeyCode::Abort => KeyCode::Abort,
240        WinitKeyCode::Resume => KeyCode::Resume,
241        WinitKeyCode::Suspend => KeyCode::Suspend,
242        WinitKeyCode::Again => KeyCode::Again,
243        WinitKeyCode::Copy => KeyCode::Copy,
244        WinitKeyCode::Cut => KeyCode::Cut,
245        WinitKeyCode::Find => KeyCode::Find,
246        WinitKeyCode::Open => KeyCode::Open,
247        WinitKeyCode::Paste => KeyCode::Paste,
248        WinitKeyCode::Props => KeyCode::Props,
249        WinitKeyCode::Select => KeyCode::Select,
250        WinitKeyCode::Undo => KeyCode::Undo,
251        WinitKeyCode::Hiragana => KeyCode::Hiragana,
252        WinitKeyCode::Katakana => KeyCode::Katakana,
253        WinitKeyCode::F1 => KeyCode::F1,
254        WinitKeyCode::F2 => KeyCode::F2,
255        WinitKeyCode::F3 => KeyCode::F3,
256        WinitKeyCode::F4 => KeyCode::F4,
257        WinitKeyCode::F5 => KeyCode::F5,
258        WinitKeyCode::F6 => KeyCode::F6,
259        WinitKeyCode::F7 => KeyCode::F7,
260        WinitKeyCode::F8 => KeyCode::F8,
261        WinitKeyCode::F9 => KeyCode::F9,
262        WinitKeyCode::F10 => KeyCode::F10,
263        WinitKeyCode::F11 => KeyCode::F11,
264        WinitKeyCode::F12 => KeyCode::F12,
265        WinitKeyCode::F13 => KeyCode::F13,
266        WinitKeyCode::F14 => KeyCode::F14,
267        WinitKeyCode::F15 => KeyCode::F15,
268        WinitKeyCode::F16 => KeyCode::F16,
269        WinitKeyCode::F17 => KeyCode::F17,
270        WinitKeyCode::F18 => KeyCode::F18,
271        WinitKeyCode::F19 => KeyCode::F19,
272        WinitKeyCode::F20 => KeyCode::F20,
273        WinitKeyCode::F21 => KeyCode::F21,
274        WinitKeyCode::F22 => KeyCode::F22,
275        WinitKeyCode::F23 => KeyCode::F23,
276        WinitKeyCode::F24 => KeyCode::F24,
277        WinitKeyCode::F25 => KeyCode::F25,
278        WinitKeyCode::F26 => KeyCode::F26,
279        WinitKeyCode::F27 => KeyCode::F27,
280        WinitKeyCode::F28 => KeyCode::F28,
281        WinitKeyCode::F29 => KeyCode::F29,
282        WinitKeyCode::F30 => KeyCode::F30,
283        WinitKeyCode::F31 => KeyCode::F31,
284        WinitKeyCode::F32 => KeyCode::F32,
285        WinitKeyCode::F33 => KeyCode::F33,
286        WinitKeyCode::F34 => KeyCode::F34,
287        WinitKeyCode::F35 => KeyCode::F35,
288        // winit::KeyCode is `#[non_exhaustive]` — newer winit releases may
289        // add variants we don't yet know about. Rather than fall through
290        // silently, emit a one-shot warning and surface the gap to the
291        // engine via `KeyCode::Unidentified`.
292        other => {
293            static WARNED: OnceLock<()> = OnceLock::new();
294            if WARNED.set(()).is_ok() {
295                log::warn!(
296                    "khora-infra: unmapped winit::KeyCode variant {:?} (using KeyCode::Unidentified). \
297                     Consider extending map_keycode in crates/khora-infra/src/platform/input.rs.",
298                    other,
299                );
300            }
301            KeyCode::Unidentified
302        }
303    }
304}
305
306fn map_mouse_button(button: WinitMouseButton) -> MouseButton {
307    match button {
308        WinitMouseButton::Left => MouseButton::Left,
309        WinitMouseButton::Right => MouseButton::Right,
310        WinitMouseButton::Middle => MouseButton::Middle,
311        WinitMouseButton::Back => MouseButton::Back,
312        WinitMouseButton::Forward => MouseButton::Forward,
313        WinitMouseButton::Other(id) => MouseButton::Other(id),
314    }
315}
316
317// --- Unit Tests ---
318#[cfg(test)]
319mod tests {
320    use super::*;
321    use winit::{dpi::PhysicalPosition, event::WindowEvent};
322
323    #[test]
324    fn test_map_keycode_simple() {
325        assert_eq!(map_keycode(WinitKeyCode::KeyA), KeyCode::KeyA);
326        assert_eq!(map_keycode(WinitKeyCode::Digit1), KeyCode::Digit1);
327        assert_eq!(map_keycode(WinitKeyCode::Space), KeyCode::Space);
328        assert_eq!(map_keycode(WinitKeyCode::ShiftLeft), KeyCode::ShiftLeft);
329        assert_eq!(map_keycode(WinitKeyCode::F12), KeyCode::F12);
330    }
331
332    #[test]
333    fn test_map_mouse_button_standard() {
334        assert_eq!(map_mouse_button(WinitMouseButton::Left), MouseButton::Left);
335        assert_eq!(
336            map_mouse_button(WinitMouseButton::Right),
337            MouseButton::Right
338        );
339        assert_eq!(
340            map_mouse_button(WinitMouseButton::Middle),
341            MouseButton::Middle
342        );
343        assert_eq!(map_mouse_button(WinitMouseButton::Back), MouseButton::Back);
344        assert_eq!(
345            map_mouse_button(WinitMouseButton::Forward),
346            MouseButton::Forward
347        );
348    }
349
350    #[test]
351    fn test_map_mouse_button_other() {
352        assert_eq!(
353            map_mouse_button(WinitMouseButton::Other(8)),
354            MouseButton::Other(8)
355        );
356    }
357
358    #[test]
359    fn test_translate_mouse_button_pressed() {
360        let winit_event = WindowEvent::MouseInput {
361            device_id: winit::event::DeviceId::dummy(),
362            state: ElementState::Pressed,
363            button: WinitMouseButton::Left,
364        };
365        assert_eq!(
366            translate_winit_input(&winit_event),
367            Some(InputEvent::MouseButtonPressed {
368                button: MouseButton::Left,
369            })
370        );
371    }
372
373    #[test]
374    fn test_translate_cursor_moved() {
375        let winit_event = WindowEvent::CursorMoved {
376            device_id: winit::event::DeviceId::dummy(),
377            position: PhysicalPosition::new(100.5, 200.75),
378        };
379        assert_eq!(
380            translate_winit_input(&winit_event),
381            Some(InputEvent::MouseMoved {
382                x: 100.5,
383                y: 200.75,
384            })
385        );
386    }
387
388    #[test]
389    fn test_translate_mouse_wheel_line() {
390        let winit_event = WindowEvent::MouseWheel {
391            device_id: winit::event::DeviceId::dummy(),
392            delta: MouseScrollDelta::LineDelta(-1.0, 2.0),
393            phase: winit::event::TouchPhase::Moved,
394        };
395        assert_eq!(
396            translate_winit_input(&winit_event),
397            Some(InputEvent::MouseWheelScrolled {
398                delta_x: -1.0,
399                delta_y: 2.0,
400            })
401        );
402    }
403
404    #[test]
405    fn test_translate_non_input_returns_none() {
406        let winit_event_resize = WindowEvent::Resized(winit::dpi::PhysicalSize::new(100, 100));
407        assert_eq!(translate_winit_input(&winit_event_resize), None);
408    }
409}