Skip to main content

servoshell/desktop/
gamepad.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use 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    /// Handle updates to connected gamepads from GilRs
111    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                // We only want to send this for a valid digital button, aka on/off only
123                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                // We only want to send this for a valid digital button, aka on/off only
131                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                // We only want to send this for a valid non-digital button, aka the triggers
139                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                // Map axis index and value to represent Standard Gamepad axis
146                // <https://www.w3.org/TR/gamepad/#dfn-represents-a-standard-gamepad-axis>
147                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, // Other axes do not map to "standard" gamepad mapping and are ignored
153                };
154                if mapped_axis < 4 {
155                    // The Gamepad spec designates down as positive and up as negative.
156                    // GilRs does the inverse of this, so correct for it here.
157                    let axis_value = match mapped_axis {
158                        0 | 2 => value,
159                        1 | 3 => -value,
160                        _ => 0., // Should not reach here
161                    };
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                // GilRs does not yet support trigger rumble
172                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    // Map button index and value to represent Standard Gamepad button
195    // <https://www.w3.org/TR/gamepad/#dfn-represents-a-standard-gamepad-button>
196    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, // Other buttons do not map to "standard" gamepad mapping and are ignored
216        }
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}