Skip to main content

concinnity_core/render/
input.rs

1//! Backend-agnostic input snapshot returned by RenderBackend::take_input.
2//! Each backend was previously carrying its own structurally-identical
3//! InputState; this single type replaces those duplicates.
4
5/// Accumulated input state since the last poll. Drained and reset every
6/// frame by GraphicsSystem and converted into a FrameInput component for
7/// Camera3DSystem to consume.
8#[derive(Default, Debug, Clone, Copy)]
9pub struct RenderInput {
10    /// Forward movement key held.
11    pub forward: bool,
12    /// Backward movement key held.
13    pub backward: bool,
14    /// Left strafe key held.
15    pub left: bool,
16    /// Right strafe key held.
17    pub right: bool,
18    /// Sprint key held.
19    pub sprint: bool,
20    /// True for exactly one frame per interact-key press.
21    pub interact: bool,
22    /// True for exactly one frame per jump-key press.
23    pub jump: bool,
24    /// True while the Control key is held. A UI modifier (a story fast-forwards
25    /// its dialogue while it is down); not gated by menu state, like `escape` and
26    /// `captured_key`. Wired on Metal; DirectX / Vulkan set it from their key
27    /// callbacks.
28    pub ctrl: bool,
29    /// True while the Option/Alt key is held. A UI modifier (the editor's orbit
30    /// drag); not gated by menu state, like `ctrl`. Wired on Metal; DirectX /
31    /// Vulkan set it from their key callbacks.
32    pub alt: bool,
33    /// True while the platform's command modifier is held: the Command key on
34    /// macOS, where it is the idiomatic modifier for an application shortcut.
35    /// Windows and Linux leave this false and keep Ctrl as their shortcut
36    /// modifier, because the Super key there belongs to the desktop shell.
37    pub cmd: bool,
38    /// Accumulated mouse delta since the last take_input() call.
39    pub mouse_dx: f32,
40    /// Accumulated vertical mouse delta since the last take_input().
41    pub mouse_dy: f32,
42    /// Accumulated vertical scroll-wheel delta since the last take_input().
43    /// Only delivered while the cursor is free.
44    pub scroll_delta: f32,
45    /// Absolute cursor position in window pixels (origin top-left).
46    /// Only meaningful when the cursor is not captured.
47    pub mouse_x: f32,
48    /// Absolute cursor y in window pixels, origin top-left.
49    pub mouse_y: f32,
50    /// True for exactly one frame when the left mouse button is pressed
51    /// while the cursor is not captured.
52    pub left_click: bool,
53    /// True while the left mouse button is held (cursor not captured). Persists
54    /// across frames until release so a UI drag can track the cursor.
55    pub left_button_down: bool,
56    /// True for exactly one frame when the right mouse button is pressed
57    /// while the cursor is not captured. Wired on Metal; DirectX / Vulkan set
58    /// it from their mouse callbacks.
59    pub right_click: bool,
60    /// True for exactly one frame when the HUD-toggle key is pressed (F1).
61    pub hud_toggle: bool,
62    /// True for exactly one frame when Escape is pressed while the cursor is
63    /// not captured. (In captured-cursor worlds Escape continues to release
64    /// the cursor, as before, and this pulse stays false.)
65    pub escape: bool,
66    /// The canonical key pressed this poll, for the settings-menu rebind
67    /// capture, or `None`. A one-frame pulse, surfaced regardless of menu /
68    /// capture state. Wired on Metal; DirectX / Vulkan set it from their key
69    /// callbacks.
70    pub captured_key: Option<crate::components::InputKey>,
71    /// The printable character produced by this poll's key press (with the OS's
72    /// shift / dead-key / layout handling applied), for text-input fields, or
73    /// `None`. A one-frame pulse like `captured_key`, ungated by menu / capture
74    /// state. Editing keys (Backspace / Delete / arrows) are not here: those
75    /// arrive via `captured_key`. Wired on Metal; DirectX / Vulkan set it from
76    /// their WM_CHAR / char callback when built on Windows / Linux.
77    pub typed_char: Option<char>,
78}
79
80/// One frame's sampled window input, taken beside the backend right after the
81/// draw (whose event pump produced it) and consumed by the input system. In
82/// serial execution it is deposited and consumed within the same tick; the
83/// pipelined driver ships it across the thread boundary instead.
84#[derive(Default, Debug, Clone, Copy)]
85pub struct InputPacket {
86    /// The raw sampled input state.
87    pub raw: RenderInput,
88    /// Whether the cursor has left the window.
89    pub cursor_outside_window: bool,
90    /// Logical window size, for UI hit-testing and overlay layout.
91    pub viewport: (f32, f32),
92}
93
94impl InputPacket {
95    /// Sample the backend's accumulated input, cursor containment, and
96    /// logical size into one packet. Called right after the draw so the
97    /// frame's event pump is reflected.
98    pub fn sample(backend: &mut dyn crate::render::backend::RenderBackend) -> Self {
99        Self {
100            raw: backend.take_input(),
101            cursor_outside_window: backend.cursor_outside_window(),
102            viewport: backend.logical_size(),
103        }
104    }
105
106    /// Fold a newer packet onto this one without losing edges: one-frame
107    /// pulses OR together, deltas accumulate, positions and held states take
108    /// the newer value. Only needed when a consumer misses a frame (startup,
109    /// a stall); steady state is one packet per tick.
110    pub fn merge_from(&mut self, newer: InputPacket) {
111        let old = self.raw;
112        let mut raw = newer.raw;
113        raw.interact |= old.interact;
114        raw.jump |= old.jump;
115        raw.left_click |= old.left_click;
116        raw.right_click |= old.right_click;
117        raw.hud_toggle |= old.hud_toggle;
118        raw.escape |= old.escape;
119        raw.mouse_dx += old.mouse_dx;
120        raw.mouse_dy += old.mouse_dy;
121        raw.scroll_delta += old.scroll_delta;
122        raw.captured_key = raw.captured_key.or(old.captured_key);
123        raw.typed_char = raw.typed_char.or(old.typed_char);
124        self.raw = raw;
125        self.cursor_outside_window = newer.cursor_outside_window;
126        self.viewport = newer.viewport;
127    }
128}
129
130// Scroll units emitted per physical wheel notch on backends whose wheel events
131// arrive as discrete notches (DirectX WM_MOUSEWHEEL, GLFW Scroll). macOS reports
132// precise (often large) scroll deltas directly, so Metal feeds scrollingDeltaY
133// raw and does not use this. The shared UI multiplies scroll_delta by its own
134// WHEEL_SCROLL_SPEED (see ui.rs), so this is scroll-delta units per notch.
135// Consumed by DirectX (WM_MOUSEWHEEL) and Vulkan (GLFW Scroll); dead on a Metal
136// build, which feeds scrollingDeltaY raw.
137pub(crate) const WHEEL_NOTCH_SCROLL_UNITS: f32 = 20.0;
138
139/// Convert a signed wheel rotation in notches (positive = rotated away from the
140/// user, i.e. scroll up) into an additive scroll_delta increment. Negated so a
141/// positive scroll_delta scrolls a panel's content up, matching
142/// FrameInput.scroll_delta's convention (see ui.rs and metal/input.rs).
143pub fn wheel_notches_to_scroll_delta(notches: f32) -> f32 {
144    -notches * WHEEL_NOTCH_SCROLL_UNITS
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    // A missed consume must not lose edges: pulses OR, deltas accumulate,
152    // positions and held state take the newer value, and an earlier one-frame
153    // Option pulse survives a newer empty poll.
154    #[test]
155    fn packet_merge_keeps_pulses_and_accumulates_deltas() {
156        let mut pending = InputPacket {
157            raw: RenderInput {
158                jump: true,
159                left_click: true,
160                mouse_dx: 2.0,
161                scroll_delta: 1.0,
162                mouse_x: 10.0,
163                left_button_down: true,
164                typed_char: Some('a'),
165                ..Default::default()
166            },
167            cursor_outside_window: true,
168            viewport: (100.0, 100.0),
169        };
170        pending.merge_from(InputPacket {
171            raw: RenderInput {
172                interact: true,
173                mouse_dx: 3.0,
174                scroll_delta: -0.5,
175                mouse_x: 42.0,
176                left_button_down: false,
177                ..Default::default()
178            },
179            cursor_outside_window: false,
180            viewport: (200.0, 150.0),
181        });
182        assert!(pending.raw.jump, "the earlier pulse survives");
183        assert!(pending.raw.left_click);
184        assert!(pending.raw.interact, "the newer pulse is present");
185        assert_eq!(pending.raw.mouse_dx, 5.0, "deltas accumulate");
186        assert_eq!(pending.raw.scroll_delta, 0.5);
187        assert_eq!(pending.raw.mouse_x, 42.0, "position takes the newer value");
188        assert!(
189            !pending.raw.left_button_down,
190            "held state takes the newer value"
191        );
192        assert_eq!(
193            pending.raw.typed_char,
194            Some('a'),
195            "the typed pulse survives"
196        );
197        assert!(!pending.cursor_outside_window);
198        assert_eq!(pending.viewport, (200.0, 150.0));
199    }
200
201    #[test]
202    fn wheel_notch_sign_and_scale() {
203        // Rotating the wheel away from the user (positive notches, "scroll up")
204        // yields a negative scroll_delta so a panel's content moves down,
205        // revealing the top.
206        assert!(wheel_notches_to_scroll_delta(1.0) < 0.0);
207        // Rotating toward the user ("scroll down") yields a positive
208        // scroll_delta so the content moves up, revealing lower rows.
209        assert!(wheel_notches_to_scroll_delta(-1.0) > 0.0);
210        // The increment scales linearly with the number of notches.
211        assert_eq!(
212            wheel_notches_to_scroll_delta(-2.0),
213            2.0 * wheel_notches_to_scroll_delta(-1.0)
214        );
215    }
216}