1use std::collections::HashMap;
6use std::sync::mpsc::{Sender, channel};
7use std::thread;
8use std::time::Duration;
9
10use gilrs::ff::{BaseEffect, BaseEffectType, Effect, EffectBuilder, Repeat, Replay, Ticks};
11use gilrs::{Event, EventType, GamepadId, Gilrs};
12use log::{debug, warn};
13use servo::{
14 GamepadDelegate, GamepadEvent, GamepadHapticEffectRequest, GamepadHapticEffectRequestType,
15 GamepadHapticEffectType, GamepadIndex, GamepadInputBounds, GamepadSupportedHapticEffects,
16 GamepadUpdateType, InputEvent, WebView,
17};
18use winit::event_loop::EventLoopProxy;
19
20use crate::desktop::event_loop::AppEvent;
21
22pub struct HapticEffect {
23 pub effect: Effect,
24 pub request: GamepadHapticEffectRequest,
25}
26
27pub(crate) struct ServoshellGamepadDelegate {
28 sender: Sender<GamepadHapticEffectRequest>,
29}
30
31impl ServoshellGamepadDelegate {
32 pub(crate) fn new(event_loop_proxy: EventLoopProxy<AppEvent>) -> Self {
33 let (tx, rx) = channel::<GamepadHapticEffectRequest>();
34
35 let _ = thread::Builder::new()
36 .name(String::from("GamepadThread"))
37 .spawn(move || {
38 let mut haptic_effects: HashMap<usize, HapticEffect> = HashMap::new();
39 let mut handle = match Gilrs::new() {
40 Ok(handle) => handle,
41 Err(error) => {
42 warn!("Error creating gamepad input connection ({error})");
43 return;
44 },
45 };
46
47 let mut connected_gamepads: Vec<GamepadId> =
48 handle.gamepads().map(|(id, _)| id).collect();
49
50 loop {
51 while let Some(event) =
52 handle.next_event_blocking(Some(Duration::from_millis(100)))
53 {
54 let id: usize = event.id.into();
55 let gamepad = handle.gamepad(event.id);
56 let name = gamepad.name();
57 let index = GamepadIndex(id);
58
59 if let Some(index) = connected_gamepads
60 .iter()
61 .position(|&gamepad_id| event.id == gamepad_id)
62 {
63 handle.insert_event(Event::new(event.id, EventType::Connected));
64 handle.insert_event(event);
65
66 connected_gamepads.swap_remove(index);
67
68 continue;
69 }
70
71 if matches!(&event.event, EventType::ForceFeedbackEffectCompleted) {
72 match haptic_effects.remove(&id) {
73 Some(haptic_effect) => haptic_effect.request.succeeded(),
74 None => warn!("Failed to find haptic effect for id {id}"),
75 }
76
77 continue;
78 }
79
80 if event_loop_proxy
81 .send_event(AppEvent::Gamepad(event, name.to_owned(), index))
82 .is_err()
83 {
84 warn!("Error sending gamepad event to event loop proxy");
85 return;
86 }
87 }
88
89 while let Ok(request) = rx.try_recv() {
90 match request.request_type() {
91 GamepadHapticEffectRequestType::Play(effect_type) => {
92 Self::play_haptic_effect(
93 &mut haptic_effects,
94 &effect_type.clone(),
95 request,
96 &mut handle,
97 );
98 },
99 GamepadHapticEffectRequestType::Stop => {
100 Self::stop_haptic_effect(&mut haptic_effects, request);
101 },
102 }
103 }
104 }
105 });
106
107 Self { sender: tx }
108 }
109
110 pub(crate) fn handle_gamepad_events(
112 &self,
113 event: Event,
114 name: String,
115 index: GamepadIndex,
116 active_webview: WebView,
117 ) {
118 let mut gamepad_event: Option<GamepadEvent> = None;
119 match event.event {
120 EventType::ButtonPressed(button, _) => {
121 let mapped_index = Self::map_gamepad_button(button);
122 if !matches!(mapped_index, 6 | 7 | 17) {
124 let update_type = GamepadUpdateType::Button(mapped_index, 1.0);
125 gamepad_event = Some(GamepadEvent::Updated(index, update_type));
126 }
127 },
128 EventType::ButtonReleased(button, _) => {
129 let mapped_index = Self::map_gamepad_button(button);
130 if !matches!(mapped_index, 6 | 7 | 17) {
132 let update_type = GamepadUpdateType::Button(mapped_index, 0.0);
133 gamepad_event = Some(GamepadEvent::Updated(index, update_type));
134 }
135 },
136 EventType::ButtonChanged(button, value, _) => {
137 let mapped_index = Self::map_gamepad_button(button);
138 if matches!(mapped_index, 6 | 7) {
140 let update_type = GamepadUpdateType::Button(mapped_index, value as f64);
141 gamepad_event = Some(GamepadEvent::Updated(index, update_type));
142 }
143 },
144 EventType::AxisChanged(axis, value, _) => {
145 let mapped_axis: usize = match axis {
148 gilrs::Axis::LeftStickX => 0,
149 gilrs::Axis::LeftStickY => 1,
150 gilrs::Axis::RightStickX => 2,
151 gilrs::Axis::RightStickY => 3,
152 _ => 4, };
154 if mapped_axis < 4 {
155 let axis_value = match mapped_axis {
158 0 | 2 => value,
159 1 | 3 => -value,
160 _ => 0., };
162 let update_type = GamepadUpdateType::Axis(mapped_axis, axis_value as f64);
163 gamepad_event = Some(GamepadEvent::Updated(index, update_type));
164 }
165 },
166 EventType::Connected => {
167 let bounds = GamepadInputBounds {
168 axis_bounds: (-1.0, 1.0),
169 button_bounds: (0.0, 1.0),
170 };
171 let supported_haptic_effects = GamepadSupportedHapticEffects {
173 supports_dual_rumble: true,
174 supports_trigger_rumble: false,
175 };
176 gamepad_event = Some(GamepadEvent::Connected(
177 index,
178 name,
179 bounds,
180 supported_haptic_effects,
181 ));
182 },
183 EventType::Disconnected => {
184 gamepad_event = Some(GamepadEvent::Disconnected(index));
185 },
186 _ => {},
187 }
188
189 if let Some(event) = gamepad_event {
190 active_webview.notify_input_event(InputEvent::Gamepad(event));
191 }
192 }
193
194 fn map_gamepad_button(button: gilrs::Button) -> usize {
197 match button {
198 gilrs::Button::South => 0,
199 gilrs::Button::East => 1,
200 gilrs::Button::West => 2,
201 gilrs::Button::North => 3,
202 gilrs::Button::LeftTrigger => 4,
203 gilrs::Button::RightTrigger => 5,
204 gilrs::Button::LeftTrigger2 => 6,
205 gilrs::Button::RightTrigger2 => 7,
206 gilrs::Button::Select => 8,
207 gilrs::Button::Start => 9,
208 gilrs::Button::LeftThumb => 10,
209 gilrs::Button::RightThumb => 11,
210 gilrs::Button::DPadUp => 12,
211 gilrs::Button::DPadDown => 13,
212 gilrs::Button::DPadLeft => 14,
213 gilrs::Button::DPadRight => 15,
214 gilrs::Button::Mode => 16,
215 _ => 17, }
217 }
218
219 fn play_haptic_effect(
220 haptic_effects: &mut HashMap<usize, HapticEffect>,
221 effect_type: &GamepadHapticEffectType,
222 request: GamepadHapticEffectRequest,
223 handle: &mut Gilrs,
224 ) {
225 let index = request.gamepad_index();
226 let GamepadHapticEffectType::DualRumble(params) = effect_type;
227
228 let Some(connected_gamepad) = handle
229 .gamepads()
230 .find(|gamepad| usize::from(gamepad.0) == index)
231 else {
232 debug!("Couldn't find connected gamepad to play haptic effect on");
233 request.failed();
234 return;
235 };
236
237 let start_delay = Ticks::from_ms(params.start_delay as u32);
238 let duration = Ticks::from_ms(params.duration as u32);
239 let strong_magnitude = (params.strong_magnitude * u16::MAX as f64).round() as u16;
240 let weak_magnitude = (params.weak_magnitude * u16::MAX as f64).round() as u16;
241
242 let scheduling = Replay {
243 after: start_delay,
244 play_for: duration,
245 with_delay: Ticks::from_ms(0),
246 };
247 let effect = EffectBuilder::new()
248 .add_effect(BaseEffect {
249 kind: BaseEffectType::Strong {
250 magnitude: strong_magnitude,
251 },
252 scheduling,
253 envelope: Default::default(),
254 })
255 .add_effect(BaseEffect {
256 kind: BaseEffectType::Weak {
257 magnitude: weak_magnitude,
258 },
259 scheduling,
260 envelope: Default::default(),
261 })
262 .repeat(Repeat::For(start_delay + duration))
263 .add_gamepad(&connected_gamepad.1)
264 .finish(handle)
265 .expect(
266 "Failed to create haptic effect, ensure connected gamepad supports force feedback.",
267 );
268
269 haptic_effects.insert(index, HapticEffect { effect, request });
270 haptic_effects[&index]
271 .effect
272 .play()
273 .expect("Failed to play haptic effect.");
274 }
275
276 fn stop_haptic_effect(
277 haptic_effects: &mut HashMap<usize, HapticEffect>,
278 request: GamepadHapticEffectRequest,
279 ) {
280 let index = request.gamepad_index();
281
282 let Some(haptic_effect) = haptic_effects.get(&index) else {
283 request.failed();
284 return;
285 };
286
287 let stopped_successfully = match haptic_effect.effect.stop() {
288 Ok(()) => true,
289 Err(e) => {
290 debug!("Failed to stop haptic effect: {:?}", e);
291 false
292 },
293 };
294 haptic_effects.remove(&index);
295
296 if stopped_successfully {
297 request.succeeded();
298 } else {
299 request.failed();
300 }
301 }
302}
303
304impl GamepadDelegate for ServoshellGamepadDelegate {
305 fn handle_haptic_effect_request(&self, request: GamepadHapticEffectRequest) {
306 if self.sender.send(request).is_err() {
307 warn!("Haptic effect couldn't be played!")
308 }
309 }
310}