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