azul_core/gamepad.rs
1//! POD types for the gamepad / game-controller surface
2//! (SUPER_PLAN_2 §1 feature 6 + research/03 §"Feature 6").
3//!
4//! Cross-platform controller input: `gilrs` on the desktop
5//! (Windows / Linux / macOS), iOS `GCController` + Android `InputDevice`
6//! on mobile (research/03). Defined here in `azul-core` so the manager +
7//! accessors cross the FFI without `azul-layout` as a dependency; the
8//! stateful side lives in `azul_layout::managers::gamepad::GamepadManager`.
9//!
10//! Poll model, like the sensors: the backend keeps a [`GamepadState`]
11//! snapshot per connected pad current, and a callback reads the latest each
12//! frame (`CallbackInfo::get_gamepad_state`) to drive movement / menus.
13//! Button + axis naming follows the SDL / gilrs "standard gamepad" mapping,
14//! so the face buttons are Xbox-style: South = A, East = B, West = X,
15//! North = Y.
16
17/// A connected gamepad's id — stable for the lifetime of the connection,
18/// assigned by the backend on connect. (gilrs `GamepadId` / the platform
19/// device id, normalised to a `u32`.)
20#[repr(C)]
21#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
22pub struct GamepadId {
23 pub id: u32,
24}
25
26/// A standard-layout gamepad button. Face buttons are Xbox-style by
27/// position (South = A / Cross, East = B / Circle, West = X / Square,
28/// North = Y / Triangle), so layouts stay consistent across vendors.
29///
30/// The discriminant order is also the bit position in
31/// [`GamepadState::buttons`] — don't reorder without bumping the ABI.
32#[repr(C)]
33#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
34pub enum GamepadButton {
35 /// Bottom face button (A / Cross).
36 South,
37 /// Right face button (B / Circle).
38 East,
39 /// Top face button (Y / Triangle).
40 North,
41 /// Left face button (X / Square).
42 West,
43 /// Left shoulder button (L1 / LB).
44 LeftBumper,
45 /// Right shoulder button (R1 / RB).
46 RightBumper,
47 /// Left trigger as a digital press (L2 / LT). Analog value: `LeftZ`.
48 LeftTrigger,
49 /// Right trigger as a digital press (R2 / RT). Analog value: `RightZ`.
50 RightTrigger,
51 /// Select / Back / Share.
52 Select,
53 /// Start / Options / Menu.
54 Start,
55 /// Vendor / guide button (Xbox / PS / Home).
56 Mode,
57 /// Left stick click (L3).
58 LeftThumb,
59 /// Right stick click (R3).
60 RightThumb,
61 /// D-pad up.
62 DPadUp,
63 /// D-pad down.
64 DPadDown,
65 /// D-pad left.
66 DPadLeft,
67 /// D-pad right.
68 DPadRight,
69 // APPENDED at the end for ABI stability — the bitset in
70 // `GamepadState::buttons` is indexed by DISCRIMINANT, so inserting any of
71 // these in the middle would silently renumber every bit above it and turn
72 // a saved keybinding into a different button.
73 /// A miscellaneous button the vendor did not standardise: the Xbox Series
74 /// share button, the DualSense create button, the Switch capture button.
75 Misc1,
76 /// Rear paddle 1 (Xbox Elite, DualSense Edge, Steam Deck).
77 Paddle1,
78 /// Rear paddle 2.
79 Paddle2,
80 /// Rear paddle 3.
81 Paddle3,
82 /// Rear paddle 4.
83 Paddle4,
84 /// The touchpad pressed as a button (DualShock 4, DualSense). Distinct
85 /// from a touch ON the pad, which is `GamepadState::touchpad`.
86 Touchpad,
87}
88
89/// A gamepad analog axis. Stick axes are in `[-1, 1]` (right / up positive);
90/// trigger axes ([`GamepadAxis::LeftZ`] / [`GamepadAxis::RightZ`]) in
91/// `[0, 1]`.
92#[repr(C)]
93#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
94pub enum GamepadAxis {
95 /// Left stick horizontal (left −1 … right +1).
96 LeftStickX,
97 /// Left stick vertical (down −1 … up +1).
98 LeftStickY,
99 /// Right stick horizontal.
100 RightStickX,
101 /// Right stick vertical.
102 RightStickY,
103 /// Left trigger pressure (0 … 1).
104 LeftZ,
105 /// Right trigger pressure (0 … 1).
106 RightZ,
107}
108
109/// Snapshot of one gamepad's state. Buttons are a bitset (bit `n` = the
110/// [`GamepadButton`] with discriminant `n`); axes are explicit fields. All
111/// POD / `Copy`, so it crosses the FFI by value.
112#[repr(C)]
113#[derive(Debug, Clone, Copy, PartialEq)]
114pub struct GamepadState {
115 // Field order is by DECREASING ALIGNMENT, not by topic. `#[repr(C)]`
116 // lays these out literally, so an align-1 bool between align-4 floats
117 // costs 3 bytes of padding each time — the FFI checker flags it, and at
118 // one GamepadState per pad per frame it is not free.
119 /// Which pad this snapshot is for.
120 pub id: GamepadId,
121 /// Pressed-button bitset — bit `n` set ⇔ the `GamepadButton` with
122 /// discriminant `n` is held. Read via [`GamepadState::is_pressed`].
123 pub buttons: u32,
124 /// Left stick X in `[-1, 1]`.
125 pub left_stick_x: f32,
126 /// Left stick Y in `[-1, 1]`.
127 pub left_stick_y: f32,
128 /// Right stick X in `[-1, 1]`.
129 pub right_stick_x: f32,
130 /// Right stick Y in `[-1, 1]`.
131 pub right_stick_y: f32,
132 /// Left trigger pressure in `[0, 1]`.
133 pub left_z: f32,
134 /// Right trigger pressure in `[0, 1]`.
135 pub right_z: f32,
136 /// Battery charge in `[0, 1]`, or `-1.0` when the pad does not report it.
137 ///
138 /// A sentinel rather than an `Option` because this struct is `#[repr(C)]`
139 /// and crosses the C ABI, where a niche-optimised Option would not be
140 /// stable. Wired pads report `-1.0`.
141 pub battery: f32,
142 /// Where a finger is on the pad's touch surface, if it has one and a
143 /// finger is down (DualShock 4, DualSense, Steam Deck).
144 ///
145 /// `x`/`y` normalized `[0, 1]` across the surface; `active` false when
146 /// nothing is touching. Not a `TouchPoint`: the pad surface is not the
147 /// screen, so its coordinates are not window coordinates and must not be
148 /// mistaken for them.
149 ///
150 /// ORIGIN IS BOTTOM-LEFT: `y` grows upward, like the thumbstick axes
151 /// beside it and unlike a window's y-down coordinates. Stated because the
152 /// underlying hardware disagrees with itself - a DualShock's raw HID
153 /// report counts y downward while the Game Controller framework normalizes
154 /// it upward - so a producer needs to be told which one this field is.
155 pub touchpad_x: f32,
156 /// See [`GamepadState::touchpad_x`].
157 pub touchpad_y: f32,
158 /// The SECOND finger on the touch surface (DualShock 4 and DualSense
159 /// track two), same coordinates as [`GamepadState::touchpad_x`]. Valid
160 /// only while `touchpad2_active`; a pinch on the pad reads both slots.
161 pub touchpad2_x: f32,
162 /// See [`GamepadState::touchpad2_x`].
163 pub touchpad2_y: f32,
164 /// Angular velocity from the pad's own gyroscope, in **rad/s**.
165 ///
166 /// Present on DualShock 4, DualSense, Switch Pro and Steam Deck. This is
167 /// the CONTROLLER's motion, not the device's — a phone's `SensorKind`
168 /// readings describe the phone, these describe the thing in your hands,
169 /// and a game that aims with gyro needs the latter.
170 pub gyro_x: f32,
171 /// See [`GamepadState::gyro_x`].
172 pub gyro_y: f32,
173 /// See [`GamepadState::gyro_x`].
174 pub gyro_z: f32,
175 /// Acceleration from the pad's own accelerometer, in **m/s²**.
176 pub accel_x: f32,
177 /// See [`GamepadState::accel_x`].
178 pub accel_y: f32,
179 /// See [`GamepadState::accel_x`].
180 pub accel_z: f32,
181 /// `false` once the pad disconnects (the manager keeps the last slot so
182 /// a callback can observe the disconnect).
183 pub connected: bool,
184 /// Whether a finger is on the pad's touch surface.
185 pub touchpad_active: bool,
186 /// Whether a second finger is on the pad's touch surface
187 /// (`touchpad2_x` / `touchpad2_y`).
188 pub touchpad2_active: bool,
189}
190
191
192impl GamepadButton {
193 /// This button's bit in [`GamepadState::buttons`].
194 #[must_use]
195 pub const fn bit(self) -> u32 {
196 1u32 << (self as u32)
197 }
198}
199
200impl Default for GamepadState {
201 fn default() -> Self {
202 Self {
203 id: GamepadId { id: 0 },
204 connected: false,
205 buttons: 0,
206 left_stick_x: 0.0,
207 left_stick_y: 0.0,
208 right_stick_x: 0.0,
209 right_stick_y: 0.0,
210 left_z: 0.0,
211 right_z: 0.0,
212 // -1.0, not 0.0: zero is a real reading meaning "flat", and a pad
213 // that does not report battery must not look like a dead one.
214 battery: -1.0,
215 touchpad_x: 0.0,
216 touchpad_y: 0.0,
217 touchpad2_x: 0.0,
218 touchpad2_y: 0.0,
219 touchpad_active: false,
220 touchpad2_active: false,
221 gyro_x: 0.0,
222 gyro_y: 0.0,
223 gyro_z: 0.0,
224 accel_x: 0.0,
225 accel_y: 0.0,
226 accel_z: 0.0,
227 }
228 }
229}
230
231impl GamepadState {
232 /// An empty (disconnected) state for `id` — all buttons up, axes zero.
233 #[must_use]
234 pub const fn empty(id: GamepadId) -> Self {
235 Self {
236 id,
237 connected: false,
238 buttons: 0,
239 left_stick_x: 0.0,
240 left_stick_y: 0.0,
241 right_stick_x: 0.0,
242 right_stick_y: 0.0,
243 left_z: 0.0,
244 right_z: 0.0,
245 // -1.0 = "does not report", so an absent battery is not mistaken
246 // for a flat one. See the field docs.
247 battery: -1.0,
248 touchpad_x: 0.0,
249 touchpad_y: 0.0,
250 touchpad2_x: 0.0,
251 touchpad2_y: 0.0,
252 touchpad_active: false,
253 touchpad2_active: false,
254 gyro_x: 0.0,
255 gyro_y: 0.0,
256 gyro_z: 0.0,
257 accel_x: 0.0,
258 accel_y: 0.0,
259 accel_z: 0.0,
260 }
261 }
262
263 /// Whether `button` is currently held.
264 #[must_use]
265 pub const fn is_pressed(&self, button: GamepadButton) -> bool {
266 self.buttons & button.bit() != 0
267 }
268
269 /// The current value of `axis` (sticks `[-1, 1]`, triggers `[0, 1]`).
270 #[must_use]
271 pub const fn axis(&self, axis: GamepadAxis) -> f32 {
272 match axis {
273 GamepadAxis::LeftStickX => self.left_stick_x,
274 GamepadAxis::LeftStickY => self.left_stick_y,
275 GamepadAxis::RightStickX => self.right_stick_x,
276 GamepadAxis::RightStickY => self.right_stick_y,
277 GamepadAxis::LeftZ => self.left_z,
278 GamepadAxis::RightZ => self.right_z,
279 }
280 }
281}
282
283// FFI Option wrapper for `CallbackInfo::get_gamepad_state(id) ->
284// Option<GamepadState>` (mirrors `OptionSensorReading`).
285impl_option!(
286 GamepadState,
287 OptionGamepadState,
288 [Debug, Clone, Copy, PartialEq]
289);
290
291#[cfg(test)]
292#[path = "gamepad_test.rs"]
293mod gamepad_test;