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::cell::RefCell;
6use std::collections::HashMap;
7
8use gilrs::ff::{BaseEffect, BaseEffectType, Effect, EffectBuilder, Repeat, Replay, Ticks};
9use gilrs::{EventType, Gilrs};
10use log::{debug, warn};
11use servo::{
12    GamepadDelegate, GamepadEvent, GamepadHapticEffectRequest, GamepadHapticEffectRequestType,
13    GamepadHapticEffectType, GamepadIndex, GamepadInputBounds, GamepadSupportedHapticEffects,
14    GamepadUpdateType, InputEvent, WebView,
15};
16
17pub struct HapticEffect {
18    pub effect: Effect,
19    pub request: GamepadHapticEffectRequest,
20}
21
22pub(crate) struct ServoshellGamepadDelegate {
23    handle: RefCell<Gilrs>,
24    haptic_effects: RefCell<HashMap<usize, HapticEffect>>,
25}
26
27impl ServoshellGamepadDelegate {
28    pub(crate) fn maybe_new() -> Option<Self> {
29        let handle = match Gilrs::new() {
30            Ok(handle) => handle,
31            Err(error) => {
32                warn!("Error creating gamepad input connection ({error})");
33                return None;
34            },
35        };
36        Some(Self {
37            handle: RefCell::new(handle),
38            haptic_effects: RefCell::new(Default::default()),
39        })
40    }
41
42    /// Handle updates to connected gamepads from GilRs
43    pub(crate) fn handle_gamepad_events(&self, active_webview: WebView) {
44        let mut handle = self.handle.borrow_mut();
45        while let Some(event) = handle.next_event() {
46            let gamepad = handle.gamepad(event.id);
47            let name = gamepad.name();
48            let index = GamepadIndex(event.id.into());
49            let mut gamepad_event: Option<GamepadEvent> = None;
50            match event.event {
51                EventType::ButtonPressed(button, _) => {
52                    let mapped_index = Self::map_gamepad_button(button);
53                    // We only want to send this for a valid digital button, aka on/off only
54                    if !matches!(mapped_index, 6 | 7 | 17) {
55                        let update_type = GamepadUpdateType::Button(mapped_index, 1.0);
56                        gamepad_event = Some(GamepadEvent::Updated(index, update_type));
57                    }
58                },
59                EventType::ButtonReleased(button, _) => {
60                    let mapped_index = Self::map_gamepad_button(button);
61                    // We only want to send this for a valid digital button, aka on/off only
62                    if !matches!(mapped_index, 6 | 7 | 17) {
63                        let update_type = GamepadUpdateType::Button(mapped_index, 0.0);
64                        gamepad_event = Some(GamepadEvent::Updated(index, update_type));
65                    }
66                },
67                EventType::ButtonChanged(button, value, _) => {
68                    let mapped_index = Self::map_gamepad_button(button);
69                    // We only want to send this for a valid non-digital button, aka the triggers
70                    if matches!(mapped_index, 6 | 7) {
71                        let update_type = GamepadUpdateType::Button(mapped_index, value as f64);
72                        gamepad_event = Some(GamepadEvent::Updated(index, update_type));
73                    }
74                },
75                EventType::AxisChanged(axis, value, _) => {
76                    // Map axis index and value to represent Standard Gamepad axis
77                    // <https://www.w3.org/TR/gamepad/#dfn-represents-a-standard-gamepad-axis>
78                    let mapped_axis: usize = match axis {
79                        gilrs::Axis::LeftStickX => 0,
80                        gilrs::Axis::LeftStickY => 1,
81                        gilrs::Axis::RightStickX => 2,
82                        gilrs::Axis::RightStickY => 3,
83                        _ => 4, // Other axes do not map to "standard" gamepad mapping and are ignored
84                    };
85                    if mapped_axis < 4 {
86                        // The Gamepad spec designates down as positive and up as negative.
87                        // GilRs does the inverse of this, so correct for it here.
88                        let axis_value = match mapped_axis {
89                            0 | 2 => value,
90                            1 | 3 => -value,
91                            _ => 0., // Should not reach here
92                        };
93                        let update_type = GamepadUpdateType::Axis(mapped_axis, axis_value as f64);
94                        gamepad_event = Some(GamepadEvent::Updated(index, update_type));
95                    }
96                },
97                EventType::Connected => {
98                    let name = String::from(name);
99                    let bounds = GamepadInputBounds {
100                        axis_bounds: (-1.0, 1.0),
101                        button_bounds: (0.0, 1.0),
102                    };
103                    // GilRs does not yet support trigger rumble
104                    let supported_haptic_effects = GamepadSupportedHapticEffects {
105                        supports_dual_rumble: true,
106                        supports_trigger_rumble: false,
107                    };
108                    gamepad_event = Some(GamepadEvent::Connected(
109                        index,
110                        name,
111                        bounds,
112                        supported_haptic_effects,
113                    ));
114                },
115                EventType::Disconnected => {
116                    gamepad_event = Some(GamepadEvent::Disconnected(index));
117                },
118                EventType::ForceFeedbackEffectCompleted => {
119                    if let Some(haptic_effect) =
120                        self.haptic_effects.borrow_mut().remove(&event.id.into())
121                    {
122                        haptic_effect.request.succeeded();
123                    } else {
124                        warn!("Failed to find haptic effect for id {}", event.id);
125                    }
126                },
127                _ => {},
128            }
129
130            if let Some(event) = gamepad_event {
131                active_webview.notify_input_event(InputEvent::Gamepad(event));
132            }
133        }
134    }
135
136    // Map button index and value to represent Standard Gamepad button
137    // <https://www.w3.org/TR/gamepad/#dfn-represents-a-standard-gamepad-button>
138    fn map_gamepad_button(button: gilrs::Button) -> usize {
139        match button {
140            gilrs::Button::South => 0,
141            gilrs::Button::East => 1,
142            gilrs::Button::West => 2,
143            gilrs::Button::North => 3,
144            gilrs::Button::LeftTrigger => 4,
145            gilrs::Button::RightTrigger => 5,
146            gilrs::Button::LeftTrigger2 => 6,
147            gilrs::Button::RightTrigger2 => 7,
148            gilrs::Button::Select => 8,
149            gilrs::Button::Start => 9,
150            gilrs::Button::LeftThumb => 10,
151            gilrs::Button::RightThumb => 11,
152            gilrs::Button::DPadUp => 12,
153            gilrs::Button::DPadDown => 13,
154            gilrs::Button::DPadLeft => 14,
155            gilrs::Button::DPadRight => 15,
156            gilrs::Button::Mode => 16,
157            _ => 17, // Other buttons do not map to "standard" gamepad mapping and are ignored
158        }
159    }
160
161    fn play_haptic_effect(
162        &self,
163        effect_type: &GamepadHapticEffectType,
164        request: GamepadHapticEffectRequest,
165    ) {
166        let index = request.gamepad_index();
167        let GamepadHapticEffectType::DualRumble(params) = effect_type;
168
169        let mut handle = self.handle.borrow_mut();
170        let Some(connected_gamepad) = handle
171            .gamepads()
172            .find(|gamepad| usize::from(gamepad.0) == index)
173        else {
174            debug!("Couldn't find connected gamepad to play haptic effect on");
175            request.failed();
176            return;
177        };
178
179        let start_delay = Ticks::from_ms(params.start_delay as u32);
180        let duration = Ticks::from_ms(params.duration as u32);
181        let strong_magnitude = (params.strong_magnitude * u16::MAX as f64).round() as u16;
182        let weak_magnitude = (params.weak_magnitude * u16::MAX as f64).round() as u16;
183
184        let scheduling = Replay {
185            after: start_delay,
186            play_for: duration,
187            with_delay: Ticks::from_ms(0),
188        };
189        let effect = EffectBuilder::new()
190            .add_effect(BaseEffect {
191                kind: BaseEffectType::Strong {
192                    magnitude: strong_magnitude,
193                },
194                scheduling,
195                envelope: Default::default(),
196            })
197            .add_effect(BaseEffect {
198                kind: BaseEffectType::Weak {
199                    magnitude: weak_magnitude,
200                },
201                scheduling,
202                envelope: Default::default(),
203            })
204            .repeat(Repeat::For(start_delay + duration))
205            .add_gamepad(&connected_gamepad.1)
206            .finish(&mut handle)
207            .expect(
208                "Failed to create haptic effect, ensure connected gamepad supports force feedback.",
209            );
210
211        let mut haptic_effects = self.haptic_effects.borrow_mut();
212        haptic_effects.insert(index, HapticEffect { effect, request });
213        haptic_effects[&index]
214            .effect
215            .play()
216            .expect("Failed to play haptic effect.");
217    }
218
219    fn stop_haptic_effect(&self, request: GamepadHapticEffectRequest) {
220        let index = request.gamepad_index();
221
222        let mut haptic_effects = self.haptic_effects.borrow_mut();
223        let Some(haptic_effect) = haptic_effects.get(&index) else {
224            request.failed();
225            return;
226        };
227
228        let stopped_successfully = match haptic_effect.effect.stop() {
229            Ok(()) => true,
230            Err(e) => {
231                debug!("Failed to stop haptic effect: {:?}", e);
232                false
233            },
234        };
235        haptic_effects.remove(&index);
236
237        if stopped_successfully {
238            request.succeeded();
239        } else {
240            request.failed();
241        }
242    }
243}
244
245impl GamepadDelegate for ServoshellGamepadDelegate {
246    fn handle_haptic_effect_request(&self, request: GamepadHapticEffectRequest) {
247        match request.request_type() {
248            GamepadHapticEffectRequestType::Play(effect_type) => {
249                self.play_haptic_effect(&effect_type.clone(), request);
250            },
251            GamepadHapticEffectRequestType::Stop => {
252                self.stop_haptic_effect(request);
253            },
254        }
255    }
256}