Skip to main content

goud_engine/sdk/
input.rs

1//! # SDK Input API
2//!
3//! Provides methods on [`GoudGame`](super::GoudGame) for querying input state:
4//! keyboard, mouse, and action mapping.
5//!
6//! The input system is built on the [`InputManager`] ECS resource. The SDK
7//! wraps it so game code can query input without reaching into the ECS layer.
8//!
9//! # Availability
10//!
11//! This module requires the `native` feature (desktop platform with GLFW).
12//!
13//! # Example
14//!
15//! ```rust,ignore
16//! use goud_engine::sdk::GoudGame;
17//! use glfw::Key;
18//!
19//! let mut game = GoudGame::new(Default::default()).unwrap();
20//!
21//! // In your game loop:
22//! if game.is_key_pressed(Key::W) {
23//!     // Move forward
24//! }
25//! if game.is_key_just_pressed(Key::Space) {
26//!     // Jump (once per press)
27//! }
28//!
29//! let (mx, my) = game.mouse_position();
30//! let scroll = game.scroll_delta();
31//! ```
32
33use super::GoudGame;
34use crate::ecs::InputManager;
35
36// Re-export input types for SDK users so callers do not need to depend
37// on `glfw` or reach into `ecs::` directly.
38pub use crate::ecs::InputBinding;
39pub use glfw::{Key, MouseButton};
40
41// =============================================================================
42// Input API (annotated for FFI generation)
43// =============================================================================
44
45// NOTE: FFI wrappers are hand-written in ffi/input.rs. The `#[goud_api]`
46// attribute is omitted here to avoid duplicate `#[no_mangle]` symbol conflicts.
47impl GoudGame {
48    // =========================================================================
49    // Keyboard Input
50    // =========================================================================
51
52    /// Returns `true` if the given key is currently held down.
53    ///
54    /// # Example
55    ///
56    /// ```rust,ignore
57    /// if game.is_key_pressed(Key::W) {
58    ///     // Move forward continuously while held
59    /// }
60    /// ```
61    #[inline]
62    pub fn is_key_pressed(&self, key: Key) -> bool {
63        self.input_manager.key_pressed(key)
64    }
65
66    /// Returns `true` if the given key was pressed this frame (not held from previous frame).
67    ///
68    /// Use this for one-shot actions like jumping or menu selection.
69    #[inline]
70    pub fn is_key_just_pressed(&self, key: Key) -> bool {
71        self.input_manager.key_just_pressed(key)
72    }
73
74    /// Returns `true` if the given key was released this frame.
75    #[inline]
76    pub fn is_key_just_released(&self, key: Key) -> bool {
77        self.input_manager.key_just_released(key)
78    }
79
80    // =========================================================================
81    // Mouse Input
82    // =========================================================================
83
84    /// Returns `true` if the given mouse button is currently held down.
85    #[inline]
86    pub fn is_mouse_button_pressed(&self, button: MouseButton) -> bool {
87        self.input_manager.mouse_button_pressed(button)
88    }
89
90    /// Returns `true` if the given mouse button was pressed this frame.
91    #[inline]
92    pub fn is_mouse_button_just_pressed(&self, button: MouseButton) -> bool {
93        self.input_manager.mouse_button_just_pressed(button)
94    }
95
96    /// Returns `true` if the given mouse button was released this frame.
97    #[inline]
98    pub fn is_mouse_button_just_released(&self, button: MouseButton) -> bool {
99        self.input_manager.mouse_button_just_released(button)
100    }
101
102    /// Returns the current mouse position in window coordinates.
103    ///
104    /// Returns `(x, y)` where `(0, 0)` is the top-left corner.
105    #[inline]
106    pub fn mouse_position(&self) -> (f32, f32) {
107        let pos = self.input_manager.mouse_position();
108        (pos.x, pos.y)
109    }
110
111    /// Returns the mouse movement delta since the last frame.
112    ///
113    /// Returns `(dx, dy)` where positive X is right and positive Y is down.
114    #[inline]
115    pub fn mouse_delta(&self) -> (f32, f32) {
116        let delta = self.input_manager.mouse_delta();
117        (delta.x, delta.y)
118    }
119
120    /// Returns the scroll wheel delta since the last frame.
121    ///
122    /// Returns `(horizontal, vertical)` scroll amounts.
123    #[inline]
124    pub fn scroll_delta(&self) -> (f32, f32) {
125        let delta = self.input_manager.scroll_delta();
126        (delta.x, delta.y)
127    }
128
129    // =========================================================================
130    // Action Mapping
131    // =========================================================================
132
133    /// Maps a key to a named action.
134    ///
135    /// An action can have multiple key bindings. When any bound key is pressed,
136    /// the action is considered active.
137    ///
138    /// # Example
139    ///
140    /// ```rust,ignore
141    /// game.map_action_key("Jump", Key::Space);
142    /// game.map_action_key("Jump", Key::W);
143    ///
144    /// if game.is_action_pressed("Jump") {
145    ///     // Triggered by Space OR W
146    /// }
147    /// ```
148    #[inline]
149    pub fn map_action_key(&mut self, action: &str, key: Key) {
150        self.input_manager
151            .map_action(action, InputBinding::Key(key));
152    }
153
154    /// Maps a mouse button to a named action.
155    #[inline]
156    pub fn map_action_mouse_button(&mut self, action: &str, button: MouseButton) {
157        self.input_manager
158            .map_action(action, InputBinding::MouseButton(button));
159    }
160
161    /// Returns `true` if any binding for the named action is currently held.
162    #[inline]
163    pub fn is_action_pressed(&self, action: &str) -> bool {
164        self.input_manager.action_pressed(action)
165    }
166
167    /// Returns `true` if any binding for the named action was pressed this frame.
168    #[inline]
169    pub fn is_action_just_pressed(&self, action: &str) -> bool {
170        self.input_manager.action_just_pressed(action)
171    }
172
173    /// Returns `true` if any binding for the named action was released this frame.
174    #[inline]
175    pub fn is_action_just_released(&self, action: &str) -> bool {
176        self.input_manager.action_just_released(action)
177    }
178
179    // =========================================================================
180    // Direct InputManager Access
181    // =========================================================================
182
183    /// Returns a reference to the underlying [`InputManager`].
184    ///
185    /// Use this for advanced input queries (gamepad, input buffering, etc.)
186    /// that are not exposed through convenience methods.
187    #[inline]
188    pub fn input(&self) -> &InputManager {
189        &self.input_manager
190    }
191
192    /// Returns a mutable reference to the underlying [`InputManager`].
193    ///
194    /// Use this for advanced configuration (deadzone, buffer duration, etc.).
195    #[inline]
196    pub fn input_mut(&mut self) -> &mut InputManager {
197        &mut self.input_manager
198    }
199}
200
201// =============================================================================
202// Tests
203// =============================================================================
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208    use crate::sdk::GameConfig;
209
210    #[test]
211    fn test_input_manager_accessible() {
212        let game = GoudGame::new(GameConfig::default()).unwrap();
213        // InputManager should be initialized and accessible
214        let _input = game.input();
215    }
216
217    #[test]
218    fn test_input_manager_mutable() {
219        let mut game = GoudGame::new(GameConfig::default()).unwrap();
220        let input = game.input_mut();
221        // Should be able to configure the input manager
222        input.set_analog_deadzone(0.2);
223        assert!((input.analog_deadzone() - 0.2).abs() < 0.001);
224    }
225
226    #[test]
227    fn test_key_not_pressed_by_default() {
228        let game = GoudGame::new(GameConfig::default()).unwrap();
229        assert!(!game.is_key_pressed(Key::Space));
230        assert!(!game.is_key_just_pressed(Key::Space));
231        assert!(!game.is_key_just_released(Key::Space));
232    }
233
234    #[test]
235    fn test_mouse_button_not_pressed_by_default() {
236        let game = GoudGame::new(GameConfig::default()).unwrap();
237        assert!(!game.is_mouse_button_pressed(MouseButton::Button1));
238        assert!(!game.is_mouse_button_just_pressed(MouseButton::Button1));
239    }
240
241    #[test]
242    fn test_mouse_position_default() {
243        let game = GoudGame::new(GameConfig::default()).unwrap();
244        let (x, y) = game.mouse_position();
245        assert!((x - 0.0).abs() < 0.001);
246        assert!((y - 0.0).abs() < 0.001);
247    }
248
249    #[test]
250    fn test_action_mapping() {
251        let mut game = GoudGame::new(GameConfig::default()).unwrap();
252        game.map_action_key("Jump", Key::Space);
253        // Action exists but key is not pressed
254        assert!(!game.is_action_pressed("Jump"));
255    }
256
257    #[test]
258    fn test_key_press_and_query() {
259        let mut game = GoudGame::new(GameConfig::default()).unwrap();
260        // Simulate a key press through the input manager
261        game.input_mut().press_key(Key::W);
262        assert!(game.is_key_pressed(Key::W));
263    }
264
265    #[test]
266    fn test_scroll_delta_default() {
267        let game = GoudGame::new(GameConfig::default()).unwrap();
268        let (sx, sy) = game.scroll_delta();
269        assert!((sx - 0.0).abs() < 0.001);
270        assert!((sy - 0.0).abs() < 0.001);
271    }
272
273    #[test]
274    fn test_mouse_delta_default() {
275        let game = GoudGame::new(GameConfig::default()).unwrap();
276        let (dx, dy) = game.mouse_delta();
277        assert!((dx - 0.0).abs() < 0.001);
278        assert!((dy - 0.0).abs() < 0.001);
279    }
280}