Skip to main content

kvas_engine/
input.rs

1//! Frame input state, fed by whichever shell hosts the engine (editor or player).
2//! Besides raw keys it supports named *actions* ("Jump") bound to keys / mouse buttons.
3//! Bindings come from the project settings and can be changed from scripts at run time.
4//!
5//! The shell calls [`Input::update`] once before each runtime tick. Pressed and
6//! released edges describe differences from the previous shell snapshot; reading
7//! an edge does not consume it. [`Input::reset`] handles focus loss.
8//!
9//! ```
10//! use kvas_engine::input::{Input, Key};
11//! use glam::Vec2;
12//! let mut input = Input::default();
13//! input.rebind("Jump", &["Space"]);
14//! input.update([Key::Space].into_iter().collect(), Default::default(), Vec2::ZERO, 0.0);
15//! assert!(input.action_pressed("Jump"));
16//! input.update(Default::default(), Default::default(), Vec2::ZERO, 0.0);
17//! assert!(input.action_released("Jump"));
18//! ```
19
20use glam::Vec2;
21use std::cell::{Cell, RefCell, RefMut};
22use std::collections::HashSet;
23
24macro_rules! keys {
25    ($($k:ident),+ $(,)?) => {
26        /// Physical keyboard keys accepted by the engine shells.
27        #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
28        pub enum Key { $(#[doc = stringify!($k)] $k),+ }
29        impl Key {
30            /// Matches the names egui uses for its keys.
31            pub fn from_name(name: &str) -> Option<Key> {
32                match name { $(stringify!($k) => Some(Key::$k),)+ _ => None }
33            }
34            /// Canonical case-sensitive binding name, inverse of [`Self::from_name`].
35            pub fn name(self) -> &'static str {
36                match self { $(Key::$k => stringify!($k),)+ }
37            }
38        }
39    };
40}
41
42keys!(
43    A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z, Num0, Num1, Num2, Num3, Num4, Num5, Num6, Num7, Num8,
44    Num9, Space, Enter, Escape, Tab, ArrowLeft, ArrowRight, ArrowUp, ArrowDown, Shift, Ctrl, Alt
45);
46
47/// Supported physical pointer buttons.
48#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
49pub enum MouseButton {
50    /// Primary pointer button.
51    Left,
52    /// Secondary pointer button.
53    Right,
54    /// Wheel or middle pointer button.
55    Middle,
56}
57
58/// One physical input an action can be bound to.
59#[derive(Clone, Copy, PartialEq, Eq, Debug)]
60pub enum Source {
61    /// Keyboard source.
62    Key(Key),
63    /// Pointer source.
64    Mouse(MouseButton),
65}
66
67impl Source {
68    /// "W", "Space", "MouseLeft", ...
69    pub fn parse(name: &str) -> Option<Source> {
70        match name {
71            "MouseLeft" => Some(Source::Mouse(MouseButton::Left)),
72            "MouseRight" => Some(Source::Mouse(MouseButton::Right)),
73            "MouseMiddle" => Some(Source::Mouse(MouseButton::Middle)),
74            _ => Key::from_name(name).map(Source::Key),
75        }
76    }
77
78    /// Canonical binding string accepted by [`Self::parse`].
79    pub fn name(self) -> &'static str {
80        match self {
81            Source::Key(k) => k.name(),
82            Source::Mouse(MouseButton::Left) => "MouseLeft",
83            Source::Mouse(MouseButton::Right) => "MouseRight",
84            Source::Mouse(MouseButton::Middle) => "MouseMiddle",
85        }
86    }
87}
88
89/// Named action and its alternative physical inputs (logical OR).
90#[derive(Clone, Debug)]
91pub struct Binding {
92    /// Case-sensitive action identifier.
93    pub name: String,
94    /// Alternative keys or buttons; an empty list disables the action.
95    pub sources: Vec<Source>,
96}
97
98/// On-screen text a game can show; the shell (editor Play view or player) draws it.
99#[derive(Clone, Default, Debug)]
100pub struct Hud {
101    /// Whether the shell draws a crosshair.
102    pub crosshair: bool,
103    /// Interaction hint ("Press E to use"); cleared every frame, so set it each frame it applies.
104    pub prompt: String,
105    /// Short message (pickup, door opened) shown for `message_time` seconds.
106    pub message: String,
107    /// Remaining message lifetime in seconds.
108    pub message_time: f32,
109    /// Persistent goal text in the corner.
110    pub objective: String,
111}
112
113/// Input snapshot, editable action bindings and shell requests.
114///
115/// Interior mutability permits shared script access to bindings and the HUD.
116/// Do not hold a [`Self::hud`] borrow while calling another HUD operation.
117#[derive(Default, Clone)]
118pub struct Input {
119    down: HashSet<Key>,
120    pressed: HashSet<Key>,
121    released: HashSet<Key>,
122    mouse_down: HashSet<MouseButton>,
123    mouse_pressed: HashSet<MouseButton>,
124    mouse_released: HashSet<MouseButton>,
125    /// Pointer movement accumulated by the shell for this frame, in shell units.
126    pub mouse_delta: Vec2,
127    /// Vertical wheel movement for this frame, in shell units.
128    pub scroll: f32,
129    // Vec + linear search instead of a map: no hasher state shared with script libraries.
130    bindings: RefCell<Vec<Binding>>,
131    cursor_lock: Cell<bool>,
132    hud: RefCell<Hud>,
133    /// A scene change asked for by a script, applied by the host after the frame.
134    scene_request: RefCell<Option<SceneRequest>>,
135    /// Asset path of the running scene, kept by the host.
136    scene_path: RefCell<String>,
137    /// Screen interface drawn by scripts this frame.
138    pub canvas: crate::canvas::Canvas,
139    /// The game's data folder (saves, settings, files), set by the host.
140    storage: RefCell<crate::storage::Storage>,
141    /// Requests to the host: quit the game, toggle fullscreen.
142    quit: Cell<bool>,
143    fullscreen: Cell<Option<bool>>,
144}
145
146/// A scene change asked for by a script (see `ScriptCtx::load_scene`).
147#[derive(Clone, Debug, PartialEq)]
148pub enum SceneRequest {
149    /// Replace the running scene with this scene file.
150    Load(String),
151    /// Add every object of this scene file to the running scene.
152    Add(String),
153    /// Start the running scene again from its file.
154    Reload,
155}
156
157impl Input {
158    /// Whether a key is held in the current snapshot.
159    pub fn key_down(&self, k: Key) -> bool {
160        self.down.contains(&k)
161    }
162    /// True only on the frame the key went down.
163    pub fn key_pressed(&self, k: Key) -> bool {
164        self.pressed.contains(&k)
165    }
166    /// True only in the snapshot where a previously held key was released.
167    pub fn key_released(&self, k: Key) -> bool {
168        self.released.contains(&k)
169    }
170
171    /// Whether a pointer button is currently held.
172    pub fn mouse_down(&self, b: MouseButton) -> bool {
173        self.mouse_down.contains(&b)
174    }
175    /// True only in the snapshot where a pointer button went down.
176    pub fn mouse_pressed(&self, b: MouseButton) -> bool {
177        self.mouse_pressed.contains(&b)
178    }
179
180    /// True only in the snapshot where a pointer button was released.
181    pub fn mouse_released(&self, b: MouseButton) -> bool {
182        self.mouse_released.contains(&b)
183    }
184
185    /// Scripts ask the shell to hide the cursor and keep it centred (mouse look).
186    pub fn lock_cursor(&self, lock: bool) {
187        self.cursor_lock.set(lock);
188    }
189    /// Current cursor-lock request; the hosting shell is responsible for applying it.
190    pub fn cursor_locked(&self) -> bool {
191        self.cursor_lock.get()
192    }
193    /// Asks the host to change scenes after this frame. A later request in the same frame wins.
194    pub fn request_scene(&self, request: SceneRequest) {
195        *self.scene_request.borrow_mut() = Some(request);
196    }
197    /// Takes the pending scene change, if any (hosts call this after each tick).
198    pub fn take_scene_request(&self) -> Option<SceneRequest> {
199        self.scene_request.borrow_mut().take()
200    }
201    /// Asset path of the running scene (empty when the host did not say).
202    pub fn scene_path(&self) -> String {
203        self.scene_path.borrow().clone()
204    }
205    /// Records the asset path of the running scene (hosts call this).
206    pub fn set_scene_path(&self, path: &str) {
207        *self.scene_path.borrow_mut() = path.to_string();
208        self.storage.borrow_mut().set_scene(path);
209    }
210    /// Sets the game's data folder (hosts call this once).
211    pub fn set_storage(&self, storage: crate::storage::Storage) {
212        let scene = self.scene_path();
213        *self.storage.borrow_mut() = storage;
214        self.storage.borrow_mut().set_scene(&scene);
215    }
216    /// The game's data folder.
217    pub fn storage(&self) -> crate::storage::Storage {
218        self.storage.borrow().clone()
219    }
220    /// Asks the host to close the game.
221    pub fn request_quit(&self) {
222        self.quit.set(true);
223    }
224    /// Whether a script asked to quit (hosts poll this).
225    pub fn quit_requested(&self) -> bool {
226        self.quit.get()
227    }
228    /// Asks the host to switch fullscreen on or off.
229    pub fn request_fullscreen(&self, on: bool) {
230        self.fullscreen.set(Some(on));
231    }
232    /// Takes a pending fullscreen request (hosts poll this).
233    pub fn take_fullscreen_request(&self) -> Option<bool> {
234        self.fullscreen.take()
235    }
236    /// Mutably borrows shell HUD state.
237    ///
238    /// # Panics
239    /// Panics if another HUD borrow is still held.
240    pub fn hud(&self) -> RefMut<'_, Hud> {
241        self.hud.borrow_mut()
242    }
243    /// Called by the game loop every frame before scripts run.
244    pub fn begin_frame(&self, dt: f32) {
245        let mut h = self.hud.borrow_mut();
246        h.prompt.clear();
247        let dt = if dt.is_finite() { dt.max(0.0) } else { 0.0 };
248        h.message_time = (h.message_time - dt).max(0.0);
249    }
250
251    /// Axis from two keys: +1 when `plus` is held, -1 when `minus` is held.
252    pub fn axis(&self, minus: Key, plus: Key) -> f32 {
253        self.key_down(plus) as i32 as f32 - self.key_down(minus) as i32 as f32
254    }
255
256    fn source_down(&self, s: Source) -> bool {
257        match s {
258            Source::Key(k) => self.key_down(k),
259            Source::Mouse(b) => self.mouse_down(b),
260        }
261    }
262
263    fn source_pressed(&self, s: Source) -> bool {
264        match s {
265            Source::Key(k) => self.key_pressed(k),
266            Source::Mouse(b) => self.mouse_pressed(b),
267        }
268    }
269
270    fn with_action<R>(&self, name: &str, f: impl Fn(&Binding) -> R, default: R) -> R {
271        self.bindings.borrow().iter().find(|b| b.name == name).map_or(default, f)
272    }
273
274    /// True while any input bound to the action is held.
275    pub fn action_down(&self, name: &str) -> bool {
276        self.with_action(name, |b| b.sources.iter().any(|&s| self.source_down(s)), false)
277    }
278
279    /// True on the frame an input bound to the action went down.
280    pub fn action_pressed(&self, name: &str) -> bool {
281        self.with_action(name, |b| b.sources.iter().any(|&s| self.source_pressed(s)), false)
282    }
283
284    /// True when the last held source of an action is released.
285    /// Releasing one alternative while another is held does not release the action.
286    /// A press and release of different sources in the same snapshot keeps it active.
287    pub fn action_released(&self, name: &str) -> bool {
288        self.with_action(
289            name,
290            |b| {
291                !b.sources.iter().any(|&s| self.source_down(s))
292                    && b.sources.iter().any(|&s| match s {
293                        Source::Key(k) => self.key_released(k),
294                        Source::Mouse(button) => self.mouse_released(button),
295                    })
296            },
297            false,
298        )
299    }
300
301    /// Two-dimensional action vector, clamped to unit length to avoid faster diagonals.
302    /// Positive X is `right`, positive Y is `up`; absent actions contribute zero.
303    pub fn action_vector(&self, left: &str, right: &str, down: &str, up: &str) -> Vec2 {
304        Vec2::new(self.action_axis(left, right), self.action_axis(down, up)).clamp_length_max(1.0)
305    }
306
307    /// Whether an action exists, including actions with no sources.
308    pub fn has_action(&self, name: &str) -> bool {
309        self.with_action(name, |_| true, false)
310    }
311
312    /// Removes an action, returning whether it existed.
313    pub fn remove_action(&self, name: &str) -> bool {
314        let mut bindings = self.bindings.borrow_mut();
315        let before = bindings.len();
316        bindings.retain(|b| b.name != name);
317        before != bindings.len()
318    }
319
320    /// Validates all input names before replacing a binding.
321    ///
322    /// Returns the first invalid name without changing existing bindings.
323    /// Unlike [`Self::rebind`], this catches configuration typos.
324    pub fn try_rebind(&self, name: &str, sources: &[&str]) -> Result<(), String> {
325        for source in sources {
326            if Source::parse(source).is_none() {
327                return Err(format!("unknown input source '{source}'"));
328            }
329        }
330        self.rebind(name, sources);
331        Ok(())
332    }
333
334    /// -1..1 from two actions, e.g. `action_axis("MoveLeft", "MoveRight")`.
335    pub fn action_axis(&self, negative: &str, positive: &str) -> f32 {
336        self.action_down(positive) as i32 as f32 - self.action_down(negative) as i32 as f32
337    }
338
339    /// Replaces (or creates) an action's bindings. Unknown names are ignored.
340    pub fn rebind(&self, name: &str, sources: &[&str]) {
341        let sources: Vec<Source> = sources.iter().filter_map(|s| Source::parse(s)).collect();
342        let mut all = self.bindings.borrow_mut();
343        match all.iter_mut().find(|b| b.name == name) {
344            Some(b) => b.sources = sources,
345            None => all.push(Binding { name: name.to_string(), sources }),
346        }
347    }
348
349    /// Current bindings as `(action, input names)`.
350    pub fn bindings(&self) -> Vec<(String, Vec<&'static str>)> {
351        self.bindings.borrow().iter().map(|b| (b.name.clone(), b.sources.iter().map(|s| s.name()).collect())).collect()
352    }
353
354    /// Replaces all actions. Duplicate action names use the last definition;
355    /// unknown physical input names are ignored as in [`Self::rebind`].
356    pub fn set_bindings<S: AsRef<str>>(&self, defs: &[(String, Vec<S>)]) {
357        self.bindings.borrow_mut().clear();
358        for (name, sources) in defs {
359            let names: Vec<&str> = sources.iter().map(|s| s.as_ref()).collect();
360            self.rebind(name, &names);
361        }
362    }
363
364    /// Call once per frame with the keys/buttons that are currently held.
365    pub fn update(&mut self, keys: HashSet<Key>, buttons: HashSet<MouseButton>, mouse_delta: Vec2, scroll: f32) {
366        self.pressed = keys.difference(&self.down).copied().collect();
367        self.released = self.down.difference(&keys).copied().collect();
368        self.down = keys;
369        self.mouse_pressed = buttons.difference(&self.mouse_down).copied().collect();
370        self.mouse_released = self.mouse_down.difference(&buttons).copied().collect();
371        self.mouse_down = buttons;
372        self.mouse_delta = mouse_delta;
373        self.scroll = scroll;
374    }
375
376    /// Clears held inputs, frame edges and pointer motion, and releases cursor lock.
377    /// Bindings and HUD content are preserved. No synthetic release edges are emitted.
378    pub fn reset(&mut self) {
379        self.down.clear();
380        self.pressed.clear();
381        self.released.clear();
382        self.mouse_down.clear();
383        self.mouse_pressed.clear();
384        self.mouse_released.clear();
385        self.mouse_delta = Vec2::ZERO;
386        self.scroll = 0.0;
387        self.cursor_lock.set(false);
388    }
389}