1use repose_core::input::{GamepadAxis, GamepadButton, GamepadEvent, GamepadId};
12
13pub trait GamepadBackend {
15 fn poll(&mut self) -> Vec<GamepadEvent>;
16 fn set_rumble(
19 &mut self,
20 _id: GamepadId,
21 _low_freq: f32,
22 _high_freq: f32,
23 _duration_ms: u32,
24 ) -> bool {
25 false
26 }
27 fn stop_rumble(&mut self, _id: GamepadId) {}
29 fn is_rumble_supported(&self, _id: GamepadId) -> bool {
31 false
32 }
33}
34
35pub const STICK_DEADZONE: f32 = 0.2;
37
38pub(crate) fn apply_stick_deadzone(v: f32) -> f32 {
39 if v.abs() < STICK_DEADZONE {
40 0.0
41 } else {
42 v.signum() * (v.abs() - STICK_DEADZONE) / (1.0 - STICK_DEADZONE)
43 }
44}
45
46#[cfg(all(feature = "gamepad", not(target_os = "android")))]
49pub struct GilrsBackend {
50 gilrs: gilrs::Gilrs,
51 ff_effects: std::collections::HashMap<u32, gilrs::ff::Effect>,
52}
53
54#[cfg(all(feature = "gamepad", not(target_os = "android")))]
55impl GilrsBackend {
56 pub fn new() -> Option<Self> {
57 match gilrs::Gilrs::new() {
58 Ok(gilrs) => Some(Self {
59 gilrs,
60 ff_effects: std::collections::HashMap::new(),
61 }),
62 Err(e) => {
63 log::warn!("gamepad: gilrs init failed ({e}); gamepad input disabled");
64 None
65 }
66 }
67 }
68
69 fn map_button(b: gilrs::Button) -> Option<GamepadButton> {
70 use gilrs::Button as G;
71 Some(match b {
72 G::South => GamepadButton::South,
73 G::East => GamepadButton::East,
74 G::West => GamepadButton::West,
75 G::North => GamepadButton::North,
76 G::Start => GamepadButton::Start,
77 G::Select => GamepadButton::Select,
78 G::LeftTrigger => GamepadButton::LeftShoulder,
79 G::RightTrigger => GamepadButton::RightShoulder,
80 G::LeftThumb => GamepadButton::LeftStick,
81 G::RightThumb => GamepadButton::RightStick,
82 G::DPadUp => GamepadButton::DPadUp,
83 G::DPadDown => GamepadButton::DPadDown,
84 G::DPadLeft => GamepadButton::DPadLeft,
85 G::DPadRight => GamepadButton::DPadRight,
86 _ => return None,
87 })
88 }
89
90 fn map_axis(a: gilrs::Axis) -> Option<GamepadAxis> {
91 use gilrs::Axis as G;
92 Some(match a {
93 G::LeftStickX => GamepadAxis::LeftStickX,
94 G::LeftStickY => GamepadAxis::LeftStickY,
95 G::RightStickX => GamepadAxis::RightStickX,
96 G::RightStickY => GamepadAxis::RightStickY,
97 _ => return None,
98 })
99 }
100}
101
102#[cfg(all(feature = "gamepad", not(target_os = "android")))]
103impl GamepadBackend for GilrsBackend {
104 fn poll(&mut self) -> Vec<GamepadEvent> {
105 use gilrs::EventType as E;
106 let mut out = Vec::new();
107 while let Some(ev) = self.gilrs.next_event() {
108 let id = GamepadId(usize::from(ev.id) as u32);
109 match ev.event {
110 E::Connected => {
111 let name = self.gilrs.gamepad(ev.id).name().to_string();
112 out.push(GamepadEvent::Connected { id, name });
113 }
114 E::Disconnected => out.push(GamepadEvent::Disconnected { id }),
115 E::ButtonPressed(b, _) | E::ButtonRepeated(b, _) => {
116 if let Some(button) = Self::map_button(b) {
117 out.push(GamepadEvent::Button {
118 id,
119 button,
120 pressed: true,
121 });
122 }
123 }
124 E::ButtonReleased(b, _) => {
125 if let Some(button) = Self::map_button(b) {
126 out.push(GamepadEvent::Button {
127 id,
128 button,
129 pressed: false,
130 });
131 }
132 }
133 E::ButtonChanged(b, v, _) => {
134 let axis = match b {
135 gilrs::Button::LeftTrigger2 => Some(GamepadAxis::LeftTrigger),
136 gilrs::Button::RightTrigger2 => Some(GamepadAxis::RightTrigger),
137 _ => None,
138 };
139 if let Some(axis) = axis {
140 out.push(GamepadEvent::Axis {
141 id,
142 axis,
143 value: v.clamp(0.0, 1.0),
144 });
145 }
146 }
147 E::AxisChanged(a, v, _) => {
148 if let Some(axis) = Self::map_axis(a) {
149 out.push(GamepadEvent::Axis {
150 id,
151 axis,
152 value: apply_stick_deadzone(v),
153 });
154 }
155 }
156 E::Dropped | E::ForceFeedbackEffectCompleted => {}
157 _ => {}
158 }
159 }
160 out
161 }
162
163 fn is_rumble_supported(&self, id: GamepadId) -> bool {
164 let want = id.0 as usize;
165 self.gilrs
166 .gamepads()
167 .find(|(gid, _)| usize::from(*gid) == want)
168 .map(|(_, pad)| pad.is_ff_supported())
169 .unwrap_or(false)
170 }
171
172 fn set_rumble(
173 &mut self,
174 id: GamepadId,
175 low_freq: f32,
176 high_freq: f32,
177 duration_ms: u32,
178 ) -> bool {
179 use gilrs::ff::{BaseEffect, BaseEffectType, EffectBuilder, Repeat, Replay, Ticks};
180 let low = low_freq.clamp(0.0, 1.0);
181 let high = high_freq.clamp(0.0, 1.0);
182 if low <= 0.0 && high <= 0.0 {
183 self.stop_rumble(id);
184 return true;
185 }
186 let want = id.0 as usize;
187 let gid = match self
188 .gilrs
189 .gamepads()
190 .find(|(gid, _)| usize::from(*gid) == want)
191 .map(|(gid, _)| gid)
192 {
193 Some(g) => g,
194 None => return false,
195 };
196 if !self
197 .gilrs
198 .connected_gamepad(gid)
199 .map(|p| p.is_ff_supported())
200 .unwrap_or(false)
201 {
202 return false;
203 }
204 let duration = Ticks::from_ms(duration_ms.max(1));
205 let mut builder = EffectBuilder::new();
206 builder
207 .add_effect(BaseEffect {
208 kind: BaseEffectType::Strong {
209 magnitude: (low * u16::MAX as f32) as u16,
210 },
211 scheduling: Replay {
212 play_for: duration,
213 ..Default::default()
214 },
215 ..Default::default()
216 })
217 .add_effect(BaseEffect {
218 kind: BaseEffectType::Weak {
219 magnitude: (high * u16::MAX as f32) as u16,
220 },
221 scheduling: Replay {
222 play_for: duration,
223 ..Default::default()
224 },
225 ..Default::default()
226 })
227 .repeat(Repeat::For(duration))
228 .gamepads(&[gid]);
229 match builder.finish(&mut self.gilrs) {
230 Ok(effect) => {
231 let _ = effect.play();
232 self.ff_effects.insert(id.0, effect);
233 true
234 }
235 Err(e) => {
236 log::warn!("gamepad: rumble failed for pad {} ({e:?})", id.0);
237 false
238 }
239 }
240 }
241
242 fn stop_rumble(&mut self, id: GamepadId) {
243 if let Some(effect) = self.ff_effects.remove(&id.0) {
244 let _ = effect.stop();
245 }
246 }
247}
248
249pub fn create_backend() -> Option<impl GamepadBackend> {
253 #[cfg(all(feature = "gamepad", target_os = "android"))]
255 {
256 AndroidBackend::new()
257 }
258 #[cfg(all(feature = "gamepad", not(target_os = "android")))]
260 {
261 GilrsBackend::new()
262 }
263 #[cfg(not(feature = "gamepad"))]
264 {
265 None::<NoBackend>
266 }
267}
268
269#[cfg(all(feature = "gamepad", target_os = "android"))]
272pub fn create_android_backend() -> Option<AndroidBackend> {
273 AndroidBackend::new()
274}
275
276#[cfg(all(feature = "gamepad", target_os = "android"))]
282pub struct AndroidBackend {
283 connected: bool,
284}
285
286#[cfg(all(feature = "gamepad", target_os = "android"))]
287impl AndroidBackend {
288 pub fn new() -> Option<Self> {
289 Some(Self { connected: false })
290 }
291
292 pub fn key_button(&mut self, code: u32, pressed: bool) -> Vec<GamepadEvent> {
297 let Some(button) = android_code_to_button(code) else {
298 return Vec::new();
299 };
300 let mut out = Vec::with_capacity(2);
301 if !self.connected {
302 self.connected = true;
303 out.push(GamepadEvent::Connected {
304 id: GamepadId(0),
305 name: "Android controller".to_string(),
306 });
307 }
308 out.push(GamepadEvent::Button {
309 id: GamepadId(0),
310 button,
311 pressed,
312 });
313 out
314 }
315}
316
317#[cfg(all(feature = "gamepad", target_os = "android"))]
318impl GamepadBackend for AndroidBackend {
319 fn poll(&mut self) -> Vec<GamepadEvent> {
320 Vec::new()
321 }
322}
323
324#[cfg(feature = "gamepad")]
325pub fn android_code_to_button(code: u32) -> Option<GamepadButton> {
326 Some(match code {
327 19 => GamepadButton::DPadUp,
328 20 => GamepadButton::DPadDown,
329 21 => GamepadButton::DPadLeft,
330 22 => GamepadButton::DPadRight,
331 23 => GamepadButton::South, 96 => GamepadButton::South, 97 => GamepadButton::East, 99 => GamepadButton::West, 100 => GamepadButton::North, 102 => GamepadButton::LeftShoulder, 103 => GamepadButton::RightShoulder, 106 => GamepadButton::LeftStick, 107 => GamepadButton::RightStick, 108 => GamepadButton::Start, 109 => GamepadButton::Select, _ => return None,
343 })
344}
345
346pub struct NoBackend;
348
349impl GamepadBackend for NoBackend {
350 fn poll(&mut self) -> Vec<GamepadEvent> {
351 Vec::new()
352 }
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358 #[test]
359 fn stick_deadzone_snaps_and_rescales() {
360 assert_eq!(apply_stick_deadzone(0.0), 0.0);
361 assert_eq!(apply_stick_deadzone(0.19), 0.0);
362 assert_eq!(apply_stick_deadzone(-0.19), 0.0);
363 assert_eq!(apply_stick_deadzone(1.0), 1.0);
364 assert_eq!(apply_stick_deadzone(-1.0), -1.0);
365 let mid = apply_stick_deadzone(0.6);
366 assert!((mid - 0.5).abs() < 1e-6);
367 }
368
369 #[test]
370 fn rumble_unsupported_by_default() {
371 let mut backend = NoBackend;
372 assert!(!backend.is_rumble_supported(GamepadId(0)));
373 assert!(!backend.set_rumble(GamepadId(0), 1.0, 1.0, 100));
374 backend.stop_rumble(GamepadId(0));
375 }
376
377 #[cfg(feature = "gamepad")]
378 #[test]
379 fn android_codes_map_to_standard_layout() {
380 assert_eq!(android_code_to_button(96), Some(GamepadButton::South));
381 assert_eq!(android_code_to_button(97), Some(GamepadButton::East));
382 assert_eq!(android_code_to_button(99), Some(GamepadButton::West));
383 assert_eq!(android_code_to_button(100), Some(GamepadButton::North));
384 assert_eq!(
385 android_code_to_button(102),
386 Some(GamepadButton::LeftShoulder)
387 );
388 assert_eq!(android_code_to_button(108), Some(GamepadButton::Start));
389 assert_eq!(android_code_to_button(19), Some(GamepadButton::DPadUp));
390 assert_eq!(android_code_to_button(23), Some(GamepadButton::South));
391 assert_eq!(android_code_to_button(29), None); assert_eq!(android_code_to_button(98), None); assert_eq!(android_code_to_button(110), None); }
396}