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    /// Window chrome overlapping the top of the render surface, in the same
93    /// logical units as `viewport` (see `RenderBackend::top_content_inset`).
94    pub top_inset: f32,
95}
96
97impl InputPacket {
98    /// Sample the backend's accumulated input, cursor containment, and
99    /// logical size into one packet. Called right after the draw so the
100    /// frame's event pump is reflected.
101    pub fn sample(backend: &mut dyn crate::render::backend::RenderBackend) -> Self {
102        Self {
103            raw: backend.take_input(),
104            cursor_outside_window: backend.cursor_outside_window(),
105            viewport: backend.logical_size(),
106            top_inset: backend.top_content_inset(),
107        }
108    }
109
110    /// Fold a newer packet onto this one without losing edges: one-frame
111    /// pulses OR together, deltas accumulate, positions and held states take
112    /// the newer value. Only needed when a consumer misses a frame (startup,
113    /// a stall); steady state is one packet per tick.
114    pub fn merge_from(&mut self, newer: InputPacket) {
115        let old = self.raw;
116        let mut raw = newer.raw;
117        raw.interact |= old.interact;
118        raw.jump |= old.jump;
119        raw.left_click |= old.left_click;
120        raw.right_click |= old.right_click;
121        raw.hud_toggle |= old.hud_toggle;
122        raw.escape |= old.escape;
123        raw.mouse_dx += old.mouse_dx;
124        raw.mouse_dy += old.mouse_dy;
125        raw.scroll_delta += old.scroll_delta;
126        raw.captured_key = raw.captured_key.or(old.captured_key);
127        raw.typed_char = raw.typed_char.or(old.typed_char);
128        self.raw = raw;
129        self.cursor_outside_window = newer.cursor_outside_window;
130        self.viewport = newer.viewport;
131        self.top_inset = newer.top_inset;
132    }
133}
134
135// Scroll units emitted per physical wheel notch on backends whose wheel events
136// arrive as discrete notches (DirectX WM_MOUSEWHEEL, GLFW Scroll). macOS reports
137// precise (often large) scroll deltas directly, so Metal feeds scrollingDeltaY
138// raw and does not use this. The shared UI multiplies scroll_delta by its own
139// WHEEL_SCROLL_SPEED (see ui.rs), so this is scroll-delta units per notch.
140// Consumed by DirectX (WM_MOUSEWHEEL) and Vulkan (GLFW Scroll); dead on a Metal
141// build, which feeds scrollingDeltaY raw.
142pub(crate) const WHEEL_NOTCH_SCROLL_UNITS: f32 = 20.0;
143
144/// Convert a signed wheel rotation in notches (positive = rotated away from the
145/// user, i.e. scroll up) into an additive scroll_delta increment. Negated so a
146/// positive scroll_delta scrolls a panel's content up, matching
147/// FrameInput.scroll_delta's convention (see ui.rs and metal/input.rs).
148pub fn wheel_notches_to_scroll_delta(notches: f32) -> f32 {
149    -notches * WHEEL_NOTCH_SCROLL_UNITS
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    // A missed consume must not lose edges: pulses OR, deltas accumulate,
157    // positions and held state take the newer value, and an earlier one-frame
158    // Option pulse survives a newer empty poll.
159    #[test]
160    fn packet_merge_keeps_pulses_and_accumulates_deltas() {
161        let mut pending = InputPacket {
162            raw: RenderInput {
163                jump: true,
164                left_click: true,
165                mouse_dx: 2.0,
166                scroll_delta: 1.0,
167                mouse_x: 10.0,
168                left_button_down: true,
169                typed_char: Some('a'),
170                ..Default::default()
171            },
172            cursor_outside_window: true,
173            viewport: (100.0, 100.0),
174            top_inset: 0.0,
175        };
176        pending.merge_from(InputPacket {
177            raw: RenderInput {
178                interact: true,
179                mouse_dx: 3.0,
180                scroll_delta: -0.5,
181                mouse_x: 42.0,
182                left_button_down: false,
183                ..Default::default()
184            },
185            cursor_outside_window: false,
186            viewport: (200.0, 150.0),
187            top_inset: 28.0,
188        });
189        assert!(pending.raw.jump, "the earlier pulse survives");
190        assert!(pending.raw.left_click);
191        assert!(pending.raw.interact, "the newer pulse is present");
192        assert_eq!(pending.raw.mouse_dx, 5.0, "deltas accumulate");
193        assert_eq!(pending.raw.scroll_delta, 0.5);
194        assert_eq!(pending.raw.mouse_x, 42.0, "position takes the newer value");
195        assert!(
196            !pending.raw.left_button_down,
197            "held state takes the newer value"
198        );
199        assert_eq!(
200            pending.raw.typed_char,
201            Some('a'),
202            "the typed pulse survives"
203        );
204        assert!(!pending.cursor_outside_window);
205        assert_eq!(pending.viewport, (200.0, 150.0));
206        assert_eq!(
207            pending.top_inset, 28.0,
208            "the window metrics take the newer value"
209        );
210    }
211
212    #[test]
213    fn wheel_notch_sign_and_scale() {
214        // Rotating the wheel away from the user (positive notches, "scroll up")
215        // yields a negative scroll_delta so a panel's content moves down,
216        // revealing the top.
217        assert!(wheel_notches_to_scroll_delta(1.0) < 0.0);
218        // Rotating toward the user ("scroll down") yields a positive
219        // scroll_delta so the content moves up, revealing lower rows.
220        assert!(wheel_notches_to_scroll_delta(-1.0) > 0.0);
221        // The increment scales linearly with the number of notches.
222        assert_eq!(
223            wheel_notches_to_scroll_delta(-2.0),
224            2.0 * wheel_notches_to_scroll_delta(-1.0)
225        );
226    }
227}