1use 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 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 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 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 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 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, };
83 if mapped_axis < 4 {
84 let axis_value = match mapped_axis {
87 0 | 2 => value,
88 1 | 3 => -value,
89 _ => 0., };
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 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 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, }
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}