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//! Provides translation from a concrete windowing backend (`winit`) to the engine's abstract input events.
16//!
17//! This module acts as an adapter layer, decoupling the rest of the engine from the
18//! specific input event format of the `winit` crate.
19
20use winit::event::{ElementState, MouseButton as WinitMouseButton, MouseScrollDelta, WindowEvent};
21use winit::keyboard::{KeyCode, PhysicalKey};
22
23/// An engine-internal representation of a user input event.
24///
25/// This enum is backend-agnostic and represents the high-level input actions
26/// that the engine's systems can respond to.
27#[derive(Debug, Clone, PartialEq)]
28pub enum InputEvent {
29    /// A keyboard key was pressed.
30    KeyPressed {
31        /// A string representation of the physical key code.
32        key_code: String,
33    },
34    /// A keyboard key was released.
35    KeyReleased {
36        /// A string representation of the physical key code.
37        key_code: String,
38    },
39    /// A mouse button was pressed.
40    MouseButtonPressed {
41        /// The mouse button that was pressed.
42        button: MouseButton,
43    },
44    /// A mouse button was released.
45    MouseButtonReleased {
46        /// The mouse button that was released.
47        button: MouseButton,
48    },
49    /// The mouse cursor moved.
50    MouseMoved {
51        /// The new x-coordinate of the cursor.
52        x: f32,
53        /// The new y-coordinate of the cursor.
54        y: f32,
55    },
56    /// The mouse wheel was scrolled.
57    MouseWheelScrolled {
58        /// The horizontal scroll delta.
59        delta_x: f32,
60        /// The vertical scroll delta.
61        delta_y: f32,
62    },
63}
64
65/// An engine-internal representation of a mouse button.
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
67pub enum MouseButton {
68    /// The left mouse button.
69    Left,
70    /// The right mouse button.
71    Right,
72    /// The middle mouse button.
73    Middle,
74    /// The back mouse button (typically on the side).
75    Back,
76    /// The forward mouse button (typically on the side).
77    Forward,
78    /// Another mouse button, identified by a numeric code.
79    Other(u16),
80}
81
82/// Translates a `winit::event::WindowEvent` into Khora's `InputEvent` format.
83///
84/// This function acts as an adapter, filtering and converting raw windowing events
85/// into a format that the engine's core systems can understand and process. It ignores
86/// events that are not direct user input actions (e.g., window resizing, focus changes).
87///
88/// # Arguments
89///
90/// * `event`: A reference to a `WindowEvent` from the `winit` library.
91///
92/// # Returns
93///
94/// Returns `Some(InputEvent)` if the event is a recognized input action, or `None` otherwise.
95pub fn translate_winit_input(event: &WindowEvent) -> Option<InputEvent> {
96    match event {
97        WindowEvent::KeyboardInput {
98            event: key_event, ..
99        } => {
100            if let PhysicalKey::Code(keycode) = key_event.physical_key {
101                let key_code_str = map_keycode_to_string(keycode);
102                match key_event.state {
103                    ElementState::Pressed if !key_event.repeat => Some(InputEvent::KeyPressed {
104                        key_code: key_code_str,
105                    }),
106                    ElementState::Released => Some(InputEvent::KeyReleased {
107                        key_code: key_code_str,
108                    }),
109                    _ => None,
110                }
111            } else {
112                None
113            }
114        }
115        WindowEvent::CursorMoved { position, .. } => Some(InputEvent::MouseMoved {
116            x: position.x as f32,
117            y: position.y as f32,
118        }),
119        WindowEvent::MouseInput { state, button, .. } => {
120            let khora_button = map_mouse_button(*button);
121            match state {
122                ElementState::Pressed => Some(InputEvent::MouseButtonPressed {
123                    button: khora_button,
124                }),
125                ElementState::Released => Some(InputEvent::MouseButtonReleased {
126                    button: khora_button,
127                }),
128            }
129        }
130        WindowEvent::MouseWheel { delta, .. } => {
131            let (dx, dy): (f32, f32) = match delta {
132                MouseScrollDelta::LineDelta(x, y) => (*x, *y),
133                MouseScrollDelta::PixelDelta(pos) => (pos.x as f32, pos.y as f32),
134            };
135            if dx != 0.0 || dy != 0.0 {
136                Some(InputEvent::MouseWheelScrolled {
137                    delta_x: dx,
138                    delta_y: dy,
139                })
140            } else {
141                None
142            }
143        }
144        _ => None,
145    }
146}
147
148// --- Private Helper Functions ---
149
150/// (Internal) Maps a `winit::keyboard::KeyCode` to a string representation.
151fn map_keycode_to_string(keycode: KeyCode) -> String {
152    format!("{keycode:?}")
153}
154
155/// (Internal) Maps a `winit::event::MouseButton` to the engine's `MouseButton` enum.
156fn map_mouse_button(button: WinitMouseButton) -> MouseButton {
157    match button {
158        WinitMouseButton::Left => MouseButton::Left,
159        WinitMouseButton::Right => MouseButton::Right,
160        WinitMouseButton::Middle => MouseButton::Middle,
161        WinitMouseButton::Back => MouseButton::Back,
162        WinitMouseButton::Forward => MouseButton::Forward,
163        WinitMouseButton::Other(id) => MouseButton::Other(id),
164    }
165}
166
167// --- Unit Tests for Input Translation ---
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use winit::{dpi::PhysicalPosition, event::WindowEvent, keyboard::KeyCode};
172
173    /// Test cases for translating keycodes to strings
174    #[test]
175    fn test_map_keycode_simple() {
176        assert_eq!(map_keycode_to_string(KeyCode::KeyA), "KeyA");
177        assert_eq!(map_keycode_to_string(KeyCode::Digit1), "Digit1");
178        assert_eq!(map_keycode_to_string(KeyCode::Space), "Space");
179    }
180
181    /// Test cases for translating mouse buttons to the engine's internal representation
182    #[test]
183    fn test_map_mouse_button_standard() {
184        assert_eq!(map_mouse_button(WinitMouseButton::Left), MouseButton::Left);
185        assert_eq!(
186            map_mouse_button(WinitMouseButton::Right),
187            MouseButton::Right
188        );
189        assert_eq!(
190            map_mouse_button(WinitMouseButton::Middle),
191            MouseButton::Middle
192        );
193        assert_eq!(map_mouse_button(WinitMouseButton::Back), MouseButton::Back);
194        assert_eq!(
195            map_mouse_button(WinitMouseButton::Forward),
196            MouseButton::Forward
197        );
198    }
199
200    /// Test cases for translating other mouse buttons to the engine's internal representation
201    #[test]
202    fn test_map_mouse_button_other() {
203        assert_eq!(
204            map_mouse_button(WinitMouseButton::Other(8)),
205            MouseButton::Other(8)
206        );
207        assert_eq!(
208            map_mouse_button(WinitMouseButton::Other(15)),
209            MouseButton::Other(15)
210        );
211    }
212
213    /// Test cases for translating winit mouse press to engine's internal representation
214    #[test]
215    fn test_translate_mouse_button_pressed() {
216        let winit_event = WindowEvent::MouseInput {
217            device_id: winit::event::DeviceId::dummy(),
218            state: ElementState::Pressed,
219            button: WinitMouseButton::Left,
220        };
221        let expected = Some(InputEvent::MouseButtonPressed {
222            button: MouseButton::Left,
223        });
224        assert_eq!(translate_winit_input(&winit_event), expected);
225    }
226
227    /// Test cases for translating winit mouse release to engine's internal representation
228    #[test]
229    fn test_translate_mouse_button_released() {
230        let winit_event = WindowEvent::MouseInput {
231            device_id: winit::event::DeviceId::dummy(),
232            state: ElementState::Released,
233            button: WinitMouseButton::Right,
234        };
235        let expected = Some(InputEvent::MouseButtonReleased {
236            button: MouseButton::Right,
237        });
238        assert_eq!(translate_winit_input(&winit_event), expected);
239    }
240
241    /// Test cases for translating winit cursor movement to engine's internal representation
242    #[test]
243    fn test_translate_cursor_moved() {
244        let winit_event = WindowEvent::CursorMoved {
245            device_id: winit::event::DeviceId::dummy(),
246            position: PhysicalPosition::new(100.5, 200.75),
247        };
248        let expected = Some(InputEvent::MouseMoved {
249            x: 100.5,
250            y: 200.75,
251        });
252        assert_eq!(translate_winit_input(&winit_event), expected);
253    }
254
255    /// Test cases for translating winit mouse wheel scroll to engine's internal representation
256    #[test]
257    fn test_translate_mouse_wheel_line() {
258        let winit_event = WindowEvent::MouseWheel {
259            device_id: winit::event::DeviceId::dummy(),
260            delta: MouseScrollDelta::LineDelta(-1.0, 2.0),
261            phase: winit::event::TouchPhase::Moved,
262        };
263        let expected = Some(InputEvent::MouseWheelScrolled {
264            delta_x: -1.0,
265            delta_y: 2.0,
266        });
267        assert_eq!(translate_winit_input(&winit_event), expected);
268    }
269
270    /// Test cases for translating winit mouse wheel scroll in pixels to engine's internal representation
271    #[test]
272    fn test_translate_mouse_wheel_pixel() {
273        let winit_event = WindowEvent::MouseWheel {
274            device_id: winit::event::DeviceId::dummy(),
275            delta: MouseScrollDelta::PixelDelta(PhysicalPosition::new(5.5, -10.0)),
276            phase: winit::event::TouchPhase::Moved,
277        };
278        let expected = Some(InputEvent::MouseWheelScrolled {
279            delta_x: 5.5,
280            delta_y: -10.0,
281        });
282        assert_eq!(translate_winit_input(&winit_event), expected);
283    }
284
285    /// Test cases for translating winit specific window events to engine's internal representation
286    #[test]
287    fn test_translate_non_input_returns_none() {
288        let winit_event_resize = WindowEvent::Resized(winit::dpi::PhysicalSize::new(100, 100));
289        let winit_event_focus = WindowEvent::Focused(true);
290        let winit_event_close = WindowEvent::CloseRequested;
291        assert_eq!(translate_winit_input(&winit_event_resize), None);
292        assert_eq!(translate_winit_input(&winit_event_focus), None);
293        assert_eq!(translate_winit_input(&winit_event_close), None);
294    }
295}