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;
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    /// Handle updates to connected gamepads from GilRs
41    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                    // We only want to send this for a valid digital button, aka on/off only
51                    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                    // We only want to send this for a valid digital button, aka on/off only
59                    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                    // We only want to send this for a valid non-digital button, aka the triggers
67                    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                    // Map axis index and value to represent Standard Gamepad axis
74                    // <https://www.w3.org/TR/gamepad/#dfn-represents-a-standard-gamepad-axis>
75                    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, // Other axes do not map to "standard" gamepad mapping and are ignored
81                    };
82                    if mapped_axis < 4 {
83                        // The Gamepad spec designates down as positive and up as negative.
84                        // GilRs does the inverse of this, so correct for it here.
85                        let axis_value = match mapped_axis {
86                            0 | 2 => value,
87                            1 | 3 => -value,
88                            _ => 0., // Should not reach here
89                        };
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                    // GilRs does not yet support trigger rumble
101                    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    // Map button index and value to represent Standard Gamepad button
136    // <https://www.w3.org/TR/gamepad/#dfn-represents-a-standard-gamepad-button>
137    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, // Other buttons do not map to "standard" gamepad mapping and are ignored
157        }
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}