Skip to main content

optic_window/
events.rs

1use optic_core::{NetworkEvents, Size2D};
2
3use crate::window::Window;
4use gilrs;
5use winit::event::{ElementState, MouseButton, MouseScrollDelta, TouchPhase, WindowEvent};
6use winit::keyboard::{ModifiersState, PhysicalKey};
7pub use winit::keyboard::KeyCode;
8
9/// Per-button state machine.
10///
11/// Tracks press/release frame numbers so callers can distinguish the exact
12/// frame a button was pressed or released, independent of polling order.
13///
14/// Used internally by [`Events`] for keyboard, mouse, and gamepad buttons.
15#[derive(Copy, Clone)]
16pub struct ButtonState {
17    pub held: bool,
18    pub press_frame: u64,
19    pub release_frame: u64,
20}
21
22/// Input action filter for frame-based queries.
23///
24/// Passed to query methods on [`Events`] to specify which edge or level
25/// of the button state should match.
26///
27/// | Variant | Behaviour |
28/// |---------|-----------|
29/// | `Pressed`  | True only on the frame the button transitions up→down |
30/// | `Released` | True only on the frame the button transitions down→up |
31/// | `Held`     | True every frame while the button is down |
32#[derive(Debug, Clone, Copy)]
33pub enum Is {
34    Pressed,
35    Released,
36    Held,
37}
38
39/// Mouse button identifier.
40#[derive(Debug, Clone, Copy)]
41pub enum Mouse {
42    Left,
43    Right,
44    Middle,
45    Back,
46    Forward,
47    Other(u16),
48}
49
50fn mouse_index(m: &Mouse) -> usize {
51    match m {
52        Mouse::Left => 0,
53        Mouse::Right => 1,
54        Mouse::Middle => 2,
55        Mouse::Back => 3,
56        Mouse::Forward => 4,
57        Mouse::Other(n) => (5 + *n as usize).min(7),
58    }
59}
60
61fn key_index(kc: &KeyCode) -> usize {
62    match kc {
63        KeyCode::KeyA => 0, KeyCode::KeyB => 1, KeyCode::KeyC => 2, KeyCode::KeyD => 3,
64        KeyCode::KeyE => 4, KeyCode::KeyF => 5, KeyCode::KeyG => 6, KeyCode::KeyH => 7,
65        KeyCode::KeyI => 8, KeyCode::KeyJ => 9, KeyCode::KeyK => 10, KeyCode::KeyL => 11,
66        KeyCode::KeyM => 12, KeyCode::KeyN => 13, KeyCode::KeyO => 14, KeyCode::KeyP => 15,
67        KeyCode::KeyQ => 16, KeyCode::KeyR => 17, KeyCode::KeyS => 18, KeyCode::KeyT => 19,
68        KeyCode::KeyU => 20, KeyCode::KeyV => 21, KeyCode::KeyW => 22, KeyCode::KeyX => 23,
69        KeyCode::KeyY => 24, KeyCode::KeyZ => 25,
70        KeyCode::Digit0 => 26, KeyCode::Digit1 => 27, KeyCode::Digit2 => 28, KeyCode::Digit3 => 29,
71        KeyCode::Digit4 => 30, KeyCode::Digit5 => 31, KeyCode::Digit6 => 32, KeyCode::Digit7 => 33,
72        KeyCode::Digit8 => 34, KeyCode::Digit9 => 35,
73        KeyCode::F1 => 36, KeyCode::F2 => 37, KeyCode::F3 => 38, KeyCode::F4 => 39,
74        KeyCode::F5 => 40, KeyCode::F6 => 41, KeyCode::F7 => 42, KeyCode::F8 => 43,
75        KeyCode::F9 => 44, KeyCode::F10 => 45, KeyCode::F11 => 46, KeyCode::F12 => 47,
76        KeyCode::F13 => 48, KeyCode::F14 => 49, KeyCode::F15 => 50, KeyCode::F16 => 51,
77        KeyCode::F17 => 52, KeyCode::F18 => 53, KeyCode::F19 => 54, KeyCode::F20 => 55,
78        KeyCode::F21 => 56, KeyCode::F22 => 57, KeyCode::F23 => 58, KeyCode::F24 => 59,
79        KeyCode::Escape => 60,
80        KeyCode::Enter => 61, KeyCode::Tab => 62, KeyCode::Space => 63, KeyCode::Backspace => 64,
81        KeyCode::Delete => 65, KeyCode::Insert => 66,
82        KeyCode::Home => 67, KeyCode::End => 68,
83        KeyCode::PageUp => 69, KeyCode::PageDown => 70,
84        KeyCode::ArrowUp => 71, KeyCode::ArrowDown => 72, KeyCode::ArrowLeft => 73, KeyCode::ArrowRight => 74,
85        KeyCode::ShiftLeft => 75, KeyCode::ShiftRight => 76,
86        KeyCode::ControlLeft => 77, KeyCode::ControlRight => 78,
87        KeyCode::AltLeft => 79, KeyCode::AltRight => 80,
88        KeyCode::SuperLeft => 81, KeyCode::SuperRight => 82,
89        KeyCode::CapsLock => 83, KeyCode::ScrollLock => 84, KeyCode::NumLock => 85,
90        KeyCode::PrintScreen => 86, KeyCode::Pause => 87,
91        KeyCode::Minus => 88, KeyCode::Equal => 89,
92        KeyCode::BracketLeft => 90, KeyCode::BracketRight => 91,
93        KeyCode::Semicolon => 92, KeyCode::Quote => 93, KeyCode::Comma => 94, KeyCode::Period => 95, KeyCode::Slash => 96,
94        KeyCode::Backslash => 97, KeyCode::IntlBackslash => 98,
95        KeyCode::Numpad0 => 99, KeyCode::Numpad1 => 100, KeyCode::Numpad2 => 101, KeyCode::Numpad3 => 102,
96        KeyCode::Numpad4 => 103, KeyCode::Numpad5 => 104, KeyCode::Numpad6 => 105, KeyCode::Numpad7 => 106,
97        KeyCode::Numpad8 => 107, KeyCode::Numpad9 => 108,
98        KeyCode::NumpadDecimal => 109, KeyCode::NumpadDivide => 110, KeyCode::NumpadMultiply => 111,
99        KeyCode::NumpadSubtract => 112, KeyCode::NumpadAdd => 113, KeyCode::NumpadEnter => 114, KeyCode::NumpadEqual => 115,
100        KeyCode::ContextMenu => 116,
101        _ => 255,
102    }
103}
104
105fn mouse_from_winit(b: MouseButton) -> Mouse {
106    match b {
107        MouseButton::Left => Mouse::Left,
108        MouseButton::Right => Mouse::Right,
109        MouseButton::Middle => Mouse::Middle,
110        MouseButton::Back => Mouse::Back,
111        MouseButton::Forward => Mouse::Forward,
112        MouseButton::Other(n) => Mouse::Other(n),
113    }
114}
115
116fn check_state(s: &ButtonState, action: Is, frame: u64) -> bool {
117    match action {
118        Is::Pressed => s.press_frame == frame,
119        Is::Released => s.release_frame == frame,
120        Is::Held => s.held,
121    }
122}
123
124/// Maximum number of supported gamepads.
125pub const MAX_GAMEPADS: usize = 4;
126
127/// Gamepad button identifier.
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub enum GamepadButton {
130    A, B, X, Y,
131    LB, RB, LT, RT,
132    Back, Start, Guide,
133    LeftStick, RightStick,
134    DPadUp, DPadDown, DPadLeft, DPadRight,
135    Other(u8),
136}
137
138/// Gamepad analog axis identifier.
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub enum GamepadAxis {
141    LeftX, LeftY,
142    RightX, RightY,
143    LeftTrigger, RightTrigger,
144}
145
146fn gamepad_button_index(b: &GamepadButton) -> usize {
147    match b {
148        GamepadButton::A => 0,
149        GamepadButton::B => 1,
150        GamepadButton::X => 2,
151        GamepadButton::Y => 3,
152        GamepadButton::LB => 4,
153        GamepadButton::RB => 5,
154        GamepadButton::LT => 6,
155        GamepadButton::RT => 7,
156        GamepadButton::Back => 8,
157        GamepadButton::Start => 9,
158        GamepadButton::Guide => 10,
159        GamepadButton::LeftStick => 11,
160        GamepadButton::RightStick => 12,
161        GamepadButton::DPadUp => 13,
162        GamepadButton::DPadDown => 14,
163        GamepadButton::DPadLeft => 15,
164        GamepadButton::DPadRight => 16,
165        GamepadButton::Other(n) => 17 + (*n as usize).min(2),
166    }
167}
168
169fn gamepad_axis_index(a: &GamepadAxis) -> usize {
170    match a {
171        GamepadAxis::LeftX => 0,
172        GamepadAxis::LeftY => 1,
173        GamepadAxis::RightX => 2,
174        GamepadAxis::RightY => 3,
175        GamepadAxis::LeftTrigger => 4,
176        GamepadAxis::RightTrigger => 5,
177    }
178}
179
180fn gamepad_button_from_gilrs(b: gilrs::Button) -> GamepadButton {
181    use gilrs::Button;
182    match b {
183        Button::South => GamepadButton::A,
184        Button::East => GamepadButton::B,
185        Button::West => GamepadButton::X,
186        Button::North => GamepadButton::Y,
187        Button::LeftTrigger => GamepadButton::LB,
188        Button::RightTrigger => GamepadButton::RB,
189        Button::LeftTrigger2 => GamepadButton::LT,
190        Button::RightTrigger2 => GamepadButton::RT,
191        Button::Select => GamepadButton::Back,
192        Button::Start => GamepadButton::Start,
193        Button::Mode => GamepadButton::Guide,
194        Button::LeftThumb => GamepadButton::LeftStick,
195        Button::RightThumb => GamepadButton::RightStick,
196        Button::DPadUp => GamepadButton::DPadUp,
197        Button::DPadDown => GamepadButton::DPadDown,
198        Button::DPadLeft => GamepadButton::DPadLeft,
199        Button::DPadRight => GamepadButton::DPadRight,
200        Button::Unknown => GamepadButton::Other(0),
201        _ => GamepadButton::Other(1),
202    }
203}
204
205fn gamepad_axis_from_gilrs(a: gilrs::Axis) -> GamepadAxis {
206    use gilrs::Axis;
207    match a {
208        Axis::LeftStickX => GamepadAxis::LeftX,
209        Axis::LeftStickY => GamepadAxis::LeftY,
210        Axis::RightStickX => GamepadAxis::RightX,
211        Axis::RightStickY => GamepadAxis::RightY,
212        Axis::LeftZ => GamepadAxis::LeftTrigger,
213        Axis::RightZ => GamepadAxis::RightTrigger,
214        _ => GamepadAxis::LeftX,
215    }
216}
217
218/// Per-frame input state snapshot.
219///
220/// `Events` holds all input state for a single frame. It is populated by calling
221/// [`process_window_event`](Events::process_window_event) and
222/// [`process_gilrs_event`](Events::process_gilrs_event) as events arrive, then
223/// frozen for the frame. At the end of the frame, [`end_frame`](Events::end_frame)
224/// advances the frame counter and clears transient state.
225///
226/// # Keyboard
227///
228/// ```
229/// use optic_window::*;
230///
231/// # let events = Events::new();
232/// if events.key(KeyCode::Space, Is::Pressed) {
233///     // space was just pressed this frame
234/// }
235/// if events.key(KeyCode::ControlLeft, Is::Held) {
236///     // ctrl is held down
237/// }
238/// ```
239///
240/// # Mouse
241///
242/// ```
243/// use optic_window::*;
244///
245/// # let mut events = Events::new();
246/// if events.mouse(Mouse::Left, Is::Pressed) {
247///     // left click this frame
248/// }
249/// ```
250///
251/// # Gamepad
252///
253/// ```
254/// use optic_window::*;
255///
256/// # let mut events = Events::new();
257/// if events.gamepad_button(0, GamepadButton::A, Is::Pressed) {
258///     // gamepad 0 pressed A this frame
259/// }
260/// let axis = events.gamepad_axis(0, GamepadAxis::LeftX);
261/// ```
262pub struct Events {
263    pub keys: [ButtonState; 256],
264    pub mouse_buttons: [ButtonState; 8],
265    pub mouse_scroll_line: Option<(f32, f32)>,
266    pub mouse_scroll_pixel: Option<(f64, f64)>,
267    pub modifiers: ModifiersState,
268    pub gamepad_connected: [bool; MAX_GAMEPADS],
269    pub gamepad_buttons: [[ButtonState; 20]; MAX_GAMEPADS],
270    pub gamepad_axes: [[f32; 6]; MAX_GAMEPADS],
271    pub resize_event: Option<Size2D>,
272    pub close_requested: bool,
273    pub focused: bool,
274    pub frame: u64,
275    pub network: NetworkEvents,
276}
277
278fn empty_buttons<const N: usize>() -> [ButtonState; N] {
279    [ButtonState { held: false, press_frame: 0, release_frame: 0 }; N]
280}
281
282impl Events {
283    /// Create a new, empty event state (frame = 1, focused = true).
284    pub fn new() -> Self {
285        Self {
286            keys: empty_buttons::<256>(),
287            mouse_buttons: empty_buttons::<8>(),
288            mouse_scroll_line: None,
289            mouse_scroll_pixel: None,
290            modifiers: ModifiersState::default(),
291            gamepad_connected: [false; MAX_GAMEPADS],
292            gamepad_buttons: [empty_buttons::<20>(); MAX_GAMEPADS],
293            gamepad_axes: [[0.0f32; 6]; MAX_GAMEPADS],
294            resize_event: None,
295            close_requested: false,
296            focused: true,
297            frame: 1,
298            network: NetworkEvents::default(),
299        }
300    }
301
302    /// Reset all state to defaults. Does not advance the frame counter.
303    pub fn clear(&mut self) {
304        self.keys = empty_buttons::<256>();
305        self.mouse_buttons = empty_buttons::<8>();
306        self.mouse_scroll_line = None;
307        self.mouse_scroll_pixel = None;
308        self.gamepad_buttons = [empty_buttons::<20>(); MAX_GAMEPADS];
309        self.gamepad_axes = [[0.0f32; 6]; MAX_GAMEPADS];
310        self.resize_event = None;
311        self.close_requested = false;
312        self.focused = true;
313        self.frame = 1;
314        self.network = NetworkEvents::default();
315    }
316
317    // ── Event processing ──────────────────────────────────────────────────
318
319    /// Process a single winit [`WindowEvent`], updating internal state.
320    ///
321    /// Called by the game loop for each event in the winit event queue.
322    pub fn process_window_event(&mut self, event: &WindowEvent, _window: &Window) {
323        match event {
324            WindowEvent::KeyboardInput { event, .. } => {
325                if let PhysicalKey::Code(kc) = event.physical_key {
326                    let idx = key_index(&kc);
327                    if idx < 256 {
328                        let s = &mut self.keys[idx];
329                        match event.state {
330                            ElementState::Pressed => {
331                                if !s.held {
332                                    s.press_frame = self.frame;
333                                }
334                                s.held = true;
335                            }
336                            ElementState::Released => {
337                                if s.held {
338                                    s.release_frame = self.frame;
339                                }
340                                s.held = false;
341                            }
342                        }
343                    }
344                }
345            }
346            WindowEvent::MouseInput { button, state, .. } => {
347                let m = mouse_from_winit(*button);
348                let idx = mouse_index(&m);
349                if idx < 8 {
350                    let s = &mut self.mouse_buttons[idx];
351                    match state {
352                        ElementState::Pressed => {
353                            if !s.held {
354                                s.press_frame = self.frame;
355                            }
356                            s.held = true;
357                        }
358                        ElementState::Released => {
359                            if s.held {
360                                s.release_frame = self.frame;
361                            }
362                            s.held = false;
363                        }
364                    }
365                }
366            }
367            WindowEvent::MouseWheel { delta, phase: TouchPhase::Moved, .. }
368            | WindowEvent::MouseWheel { delta, phase: TouchPhase::Started, .. } => {
369                match delta {
370                    MouseScrollDelta::LineDelta(x, y) => {
371                        self.mouse_scroll_line = Some((*x, *y));
372                    }
373                    MouseScrollDelta::PixelDelta(p) => {
374                        self.mouse_scroll_pixel = Some((p.x, p.y));
375                    }
376                }
377            }
378            WindowEvent::Resized(size) => {
379                self.resize_event = Some(Size2D::from(size.width, size.height));
380            }
381            WindowEvent::CloseRequested => {
382                self.close_requested = true;
383            }
384            WindowEvent::Focused(yes) => {
385                self.focused = *yes;
386            }
387            WindowEvent::ModifiersChanged(mods) => {
388                self.modifiers = mods.state();
389            }
390            _ => {}
391        }
392    }
393
394    /// Process a single gilrs gamepad event.
395    pub fn process_gilrs_event(&mut self, event: &gilrs::Event) {
396        let idx: usize = event.id.into();
397        if idx >= MAX_GAMEPADS { return; }
398
399        match &event.event {
400            gilrs::EventType::Connected => {
401                self.gamepad_connected[idx] = true;
402            }
403            gilrs::EventType::Disconnected => {
404                self.gamepad_connected[idx] = false;
405                self.gamepad_buttons[idx] = empty_buttons::<20>();
406                self.gamepad_axes[idx] = [0.0f32; 6];
407            }
408            gilrs::EventType::ButtonPressed(button, _) => {
409                let gp = gamepad_button_from_gilrs(*button);
410                let bi = gamepad_button_index(&gp);
411                let pad_btns: &mut [ButtonState; 20] = &mut self.gamepad_buttons[idx];
412                if bi < 20 {
413                    if !pad_btns[bi].held {
414                        pad_btns[bi].press_frame = self.frame;
415                    }
416                    pad_btns[bi].held = true;
417                }
418            }
419            gilrs::EventType::ButtonRepeated(button, _) => {
420                let gp = gamepad_button_from_gilrs(*button);
421                let bi = gamepad_button_index(&gp);
422                let pad_btns: &mut [ButtonState; 20] = &mut self.gamepad_buttons[idx];
423                if bi < 20 {
424                    pad_btns[bi].press_frame = self.frame;
425                }
426            }
427            gilrs::EventType::ButtonReleased(button, _) => {
428                let gp = gamepad_button_from_gilrs(*button);
429                let bi = gamepad_button_index(&gp);
430                let pad_btns: &mut [ButtonState; 20] = &mut self.gamepad_buttons[idx];
431                if bi < 20 {
432                    if pad_btns[bi].held {
433                        pad_btns[bi].release_frame = self.frame;
434                    }
435                    pad_btns[bi].held = false;
436                }
437            }
438            gilrs::EventType::AxisChanged(axis, val, _) => {
439                let ga = gamepad_axis_from_gilrs(*axis);
440                let ai = gamepad_axis_index(&ga);
441                let axes: &mut [f32; 6] = &mut self.gamepad_axes[idx];
442                if ai < 6 {
443                    axes[ai] = *val;
444                }
445            }
446            _ => {}
447        }
448    }
449
450    /// Advance the frame counter and clear per-frame transient state.
451    ///
452    /// Must be called at the end of every frame. Resets scroll deltas,
453    /// resize events, and network events. The `close_requested` flag
454    /// persists across frames (the user must manually clear it).
455    pub fn end_frame(&mut self) {
456        self.frame += 1;
457        self.mouse_scroll_line = None;
458        self.mouse_scroll_pixel = None;
459        self.resize_event = None;
460        self.network.packets.clear();
461        self.network.peers_connected.clear();
462        self.network.peers_disconnected.clear();
463    }
464
465    // ── Keyboard queries ──────────────────────────────────────────────────
466
467    /// Query a single key by [`KeyCode`] and [`Is`] action.
468    pub fn key(&self, kc: KeyCode, action: Is) -> bool {
469        let idx = key_index(&kc);
470        if idx >= 256 { return false; }
471        check_state(&self.keys[idx], action, self.frame)
472    }
473
474    /// Query a key combo: `primary` must match `action` while `modifier` is held.
475    ///
476    /// ```
477    /// use optic_window::*;
478    ///
479    /// # let events = Events::new();
480    /// if events.key_combo(KeyCode::KeyC, KeyCode::ControlLeft, Is::Pressed) {
481    ///     // Ctrl+C was pressed
482    /// }
483    /// ```
484    pub fn key_combo(&self, primary: KeyCode, modifier: KeyCode, action: Is) -> bool {
485        self.key(primary, action) && self.key(modifier, Is::Held)
486    }
487
488    /// Query multiple keys simultaneously — all must match their respective actions.
489    pub fn key_combo_n(&self, keys: &[(KeyCode, Is)]) -> bool {
490        keys.iter().all(|(kc, action)| self.key(*kc, *action))
491    }
492
493    /// True if any key matches the given action.
494    pub fn any_key(&self, action: Is) -> bool {
495        self.keys.iter().any(|s| check_state(s, action, self.frame))
496    }
497
498    // ── Mouse queries ─────────────────────────────────────────────────────
499
500    /// Query a mouse button by [`Mouse`] and [`Is`] action.
501    pub fn mouse(&self, m: Mouse, action: Is) -> bool {
502        let idx = mouse_index(&m);
503        if idx >= 8 { return false; }
504        check_state(&self.mouse_buttons[idx], action, self.frame)
505    }
506
507    /// True if any mouse button matches the given action.
508    pub fn any_mouse(&self, action: Is) -> bool {
509        self.mouse_buttons.iter().any(|s| check_state(s, action, self.frame))
510    }
511
512    // ── Gamepad queries ───────────────────────────────────────────────────
513
514    /// Default deadzone for gamepad analog axes.
515    pub const GAMEPAD_AXIS_DEADZONE: f32 = 0.15;
516
517    /// True if a gamepad with the given `id` is currently connected.
518    pub fn gamepad_connected(&self, id: usize) -> bool {
519        if id >= MAX_GAMEPADS { return false; }
520        self.gamepad_connected[id]
521    }
522
523    /// Number of currently connected gamepads.
524    pub fn gamepad_count(&self) -> usize {
525        self.gamepad_connected.iter().filter(|c| **c).count()
526    }
527
528    /// Query a gamepad button by [`GamepadButton`] and [`Is`] action.
529    pub fn gamepad_button(&self, id: usize, button: GamepadButton, action: Is) -> bool {
530        if id >= MAX_GAMEPADS { return false; }
531        let idx = gamepad_button_index(&button);
532        if idx >= 20 { return false; }
533        check_state(&self.gamepad_buttons[id][idx], action, self.frame)
534    }
535
536    /// True if any button on the given gamepad matches the action.
537    pub fn any_gamepad_button(&self, id: usize, action: Is) -> bool {
538        if id >= MAX_GAMEPADS { return false; }
539        self.gamepad_buttons[id].iter().any(|s| check_state(s, action, self.frame))
540    }
541
542    /// True if any button on any connected gamepad matches the action.
543    pub fn any_gamepad(&self, action: Is) -> bool {
544        for id in 0..MAX_GAMEPADS {
545            if self.gamepad_connected[id] && self.any_gamepad_button(id, action) {
546                return true;
547            }
548        }
549        false
550    }
551
552    /// Raw gamepad axis value (no deadzone applied).
553    pub fn gamepad_axis_raw(&self, id: usize, axis: GamepadAxis) -> f32 {
554        if id >= MAX_GAMEPADS { return 0.0; }
555        let idx = gamepad_axis_index(&axis);
556        if idx >= 6 { return 0.0; }
557        self.gamepad_axes[id][idx]
558    }
559
560    /// Gamepad axis value with the default deadzone ([`GAMEPAD_AXIS_DEADZONE`]).
561    ///
562    /// Values below the deadzone are snapped to 0.0.
563    pub fn gamepad_axis(&self, id: usize, axis: GamepadAxis) -> f32 {
564        let v = self.gamepad_axis_raw(id, axis);
565        if v.abs() < Self::GAMEPAD_AXIS_DEADZONE { 0.0 } else { v }
566    }
567
568    /// Gamepad axis value with a custom deadzone.
569    pub fn gamepad_axis_deadzoned(&self, id: usize, axis: GamepadAxis, deadzone: f32) -> f32 {
570        let v = self.gamepad_axis_raw(id, axis);
571        if v.abs() < deadzone { 0.0 } else { v }
572    }
573}