1use std::collections::HashMap;
6
7use gilrs::ff::{BaseEffect, BaseEffectType, Effect, EffectBuilder, Repeat, Replay, Ticks};
8use gilrs::{EventType, Gilrs};
9use log::{debug, warn};
10use servo::{
11 GamepadEvent, GamepadHapticEffectType, GamepadIndex, GamepadInputBounds,
12 GamepadSupportedHapticEffects, GamepadUpdateType, InputEvent, IpcSender, WebView,
13};
14
15pub struct HapticEffect {
16 pub effect: Effect,
17 pub sender: IpcSender<bool>,
18}
19
20pub(crate) struct GamepadSupport {
21 handle: Gilrs,
22 haptic_effects: HashMap<usize, HapticEffect>,
23}
24
25impl GamepadSupport {
26 pub(crate) fn maybe_new() -> Option<Self> {
27 let handle = match Gilrs::new() {
28 Ok(handle) => handle,
29 Err(error) => {
30 warn!("Error creating gamepad input connection ({error})");
31 return None;
32 },
33 };
34 Some(Self {
35 handle,
36 haptic_effects: Default::default(),
37 })
38 }
39
40 pub(crate) fn handle_gamepad_events(&mut self, active_webview: WebView) {
42 while let Some(event) = self.handle.next_event() {
43 let gamepad = self.handle.gamepad(event.id);
44 let name = gamepad.name();
45 let index = GamepadIndex(event.id.into());
46 let mut gamepad_event: Option<GamepadEvent> = None;
47 match event.event {
48 EventType::ButtonPressed(button, _) => {
49 let mapped_index = Self::map_gamepad_button(button);
50 if !matches!(mapped_index, 6 | 7 | 17) {
52 let update_type = GamepadUpdateType::Button(mapped_index, 1.0);
53 gamepad_event = Some(GamepadEvent::Updated(index, update_type));
54 }
55 },
56 EventType::ButtonReleased(button, _) => {
57 let mapped_index = Self::map_gamepad_button(button);
58 if !matches!(mapped_index, 6 | 7 | 17) {
60 let update_type = GamepadUpdateType::Button(mapped_index, 0.0);
61 gamepad_event = Some(GamepadEvent::Updated(index, update_type));
62 }
63 },
64 EventType::ButtonChanged(button, value, _) => {
65 let mapped_index = Self::map_gamepad_button(button);
66 if matches!(mapped_index, 6 | 7) {
68 let update_type = GamepadUpdateType::Button(mapped_index, value as f64);
69 gamepad_event = Some(GamepadEvent::Updated(index, update_type));
70 }
71 },
72 EventType::AxisChanged(axis, value, _) => {
73 let mapped_axis: usize = match axis {
76 gilrs::Axis::LeftStickX => 0,
77 gilrs::Axis::LeftStickY => 1,
78 gilrs::Axis::RightStickX => 2,
79 gilrs::Axis::RightStickY => 3,
80 _ => 4, };
82 if mapped_axis < 4 {
83 let axis_value = match mapped_axis {
86 0 | 2 => value,
87 1 | 3 => -value,
88 _ => 0., };
90 let update_type = GamepadUpdateType::Axis(mapped_axis, axis_value as f64);
91 gamepad_event = Some(GamepadEvent::Updated(index, update_type));
92 }
93 },
94 EventType::Connected => {
95 let name = String::from(name);
96 let bounds = GamepadInputBounds {
97 axis_bounds: (-1.0, 1.0),
98 button_bounds: (0.0, 1.0),
99 };
100 let supported_haptic_effects = GamepadSupportedHapticEffects {
102 supports_dual_rumble: true,
103 supports_trigger_rumble: false,
104 };
105 gamepad_event = Some(GamepadEvent::Connected(
106 index,
107 name,
108 bounds,
109 supported_haptic_effects,
110 ));
111 },
112 EventType::Disconnected => {
113 gamepad_event = Some(GamepadEvent::Disconnected(index));
114 },
115 EventType::ForceFeedbackEffectCompleted => {
116 let Some(effect) = self.haptic_effects.get(&event.id.into()) else {
117 warn!("Failed to find haptic effect for id {}", event.id);
118 return;
119 };
120 effect
121 .sender
122 .send(true)
123 .expect("Failed to send haptic effect completion.");
124 self.haptic_effects.remove(&event.id.into());
125 },
126 _ => {},
127 }
128
129 if let Some(event) = gamepad_event {
130 active_webview.notify_input_event(InputEvent::Gamepad(event));
131 }
132 }
133 }
134
135 fn map_gamepad_button(button: gilrs::Button) -> usize {
138 match button {
139 gilrs::Button::South => 0,
140 gilrs::Button::East => 1,
141 gilrs::Button::West => 2,
142 gilrs::Button::North => 3,
143 gilrs::Button::LeftTrigger => 4,
144 gilrs::Button::RightTrigger => 5,
145 gilrs::Button::LeftTrigger2 => 6,
146 gilrs::Button::RightTrigger2 => 7,
147 gilrs::Button::Select => 8,
148 gilrs::Button::Start => 9,
149 gilrs::Button::LeftThumb => 10,
150 gilrs::Button::RightThumb => 11,
151 gilrs::Button::DPadUp => 12,
152 gilrs::Button::DPadDown => 13,
153 gilrs::Button::DPadLeft => 14,
154 gilrs::Button::DPadRight => 15,
155 gilrs::Button::Mode => 16,
156 _ => 17, }
158 }
159
160 pub(crate) fn play_haptic_effect(
161 &mut self,
162 index: usize,
163 effect_type: GamepadHapticEffectType,
164 effect_complete_sender: IpcSender<bool>,
165 ) {
166 let GamepadHapticEffectType::DualRumble(params) = effect_type;
167 if let Some(connected_gamepad) = self
168 .handle
169 .gamepads()
170 .find(|gamepad| usize::from(gamepad.0) == index)
171 {
172 let start_delay = Ticks::from_ms(params.start_delay as u32);
173 let duration = Ticks::from_ms(params.duration as u32);
174 let strong_magnitude = (params.strong_magnitude * u16::MAX as f64).round() as u16;
175 let weak_magnitude = (params.weak_magnitude * u16::MAX as f64).round() as u16;
176
177 let scheduling = Replay {
178 after: start_delay,
179 play_for: duration,
180 with_delay: Ticks::from_ms(0),
181 };
182 let effect = EffectBuilder::new()
183 .add_effect(BaseEffect {
184 kind: BaseEffectType::Strong { magnitude: strong_magnitude },
185 scheduling,
186 envelope: Default::default(),
187 })
188 .add_effect(BaseEffect {
189 kind: BaseEffectType::Weak { magnitude: weak_magnitude },
190 scheduling,
191 envelope: Default::default(),
192 })
193 .repeat(Repeat::For(start_delay + duration))
194 .add_gamepad(&connected_gamepad.1)
195 .finish(&mut self.handle)
196 .expect("Failed to create haptic effect, ensure connected gamepad supports force feedback.");
197 self.haptic_effects.insert(
198 index,
199 HapticEffect {
200 effect,
201 sender: effect_complete_sender,
202 },
203 );
204 self.haptic_effects[&index]
205 .effect
206 .play()
207 .expect("Failed to play haptic effect.");
208 } else {
209 debug!("Couldn't find connected gamepad to play haptic effect on");
210 }
211 }
212
213 pub(crate) fn stop_haptic_effect(&mut self, index: usize) -> bool {
214 let Some(haptic_effect) = self.haptic_effects.get(&index) else {
215 return false;
216 };
217
218 let stopped_successfully = match haptic_effect.effect.stop() {
219 Ok(()) => true,
220 Err(e) => {
221 debug!("Failed to stop haptic effect: {:?}", e);
222 false
223 },
224 };
225 self.haptic_effects.remove(&index);
226
227 stopped_successfully
228 }
229}