concinnity_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::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}