1use std::cell::Cell;
6use std::rc::Rc;
7
8use dom_struct::dom_struct;
9use embedder_traits::{DualRumbleEffectParams, EmbedderMsg, GamepadSupportedHapticEffects};
10use ipc_channel::ipc;
11use ipc_channel::router::ROUTER;
12use js::rust::MutableHandleValue;
13
14use crate::dom::bindings::cell::DomRefCell;
15use crate::dom::bindings::codegen::Bindings::GamepadHapticActuatorBinding::{
16 GamepadEffectParameters, GamepadHapticActuatorMethods, GamepadHapticEffectType,
17};
18use crate::dom::bindings::codegen::Bindings::WindowBinding::Window_Binding::WindowMethods;
19use crate::dom::bindings::error::Error;
20use crate::dom::bindings::refcounted::{Trusted, TrustedPromise};
21use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object};
22use crate::dom::bindings::root::DomRoot;
23use crate::dom::bindings::str::DOMString;
24use crate::dom::bindings::utils::to_frozen_array;
25use crate::dom::promise::Promise;
26use crate::dom::window::Window;
27use crate::realms::InRealm;
28use crate::script_runtime::{CanGc, JSContext};
29use crate::task_source::SendableTaskSource;
30
31struct HapticEffectListener {
32 task_source: SendableTaskSource,
33 context: Trusted<GamepadHapticActuator>,
34}
35
36impl HapticEffectListener {
37 fn handle_stopped(&self, stopped_successfully: bool) {
38 let context = self.context.clone();
39 self.task_source
40 .queue(task!(handle_haptic_effect_stopped: move || {
41 let actuator = context.root();
42 actuator.handle_haptic_effect_stopped(stopped_successfully);
43 }));
44 }
45
46 fn handle_completed(&self, completed_successfully: bool) {
47 let context = self.context.clone();
48 self.task_source
49 .queue(task!(handle_haptic_effect_completed: move || {
50 let actuator = context.root();
51 actuator.handle_haptic_effect_completed(completed_successfully, CanGc::note());
52 }));
53 }
54}
55
56#[dom_struct]
58pub(crate) struct GamepadHapticActuator {
59 reflector_: Reflector,
60 gamepad_index: u32,
61 effects: Vec<GamepadHapticEffectType>,
63 #[ignore_malloc_size_of = "Rc is hard"]
65 playing_effect_promise: DomRefCell<Option<Rc<Promise>>>,
66 sequence_id: Cell<u32>,
71 effect_sequence_id: Cell<u32>,
73 reset_sequence_id: Cell<u32>,
75}
76
77impl GamepadHapticActuator {
78 fn new_inherited(
79 gamepad_index: u32,
80 supported_haptic_effects: GamepadSupportedHapticEffects,
81 ) -> GamepadHapticActuator {
82 let mut effects = vec![];
83 if supported_haptic_effects.supports_dual_rumble {
84 effects.push(GamepadHapticEffectType::Dual_rumble);
85 }
86 if supported_haptic_effects.supports_trigger_rumble {
87 effects.push(GamepadHapticEffectType::Trigger_rumble);
88 }
89 Self {
90 reflector_: Reflector::new(),
91 gamepad_index,
92 effects,
93 playing_effect_promise: DomRefCell::new(None),
94 sequence_id: Cell::new(0),
95 effect_sequence_id: Cell::new(0),
96 reset_sequence_id: Cell::new(0),
97 }
98 }
99
100 pub(crate) fn new(
101 window: &Window,
102 gamepad_index: u32,
103 supported_haptic_effects: GamepadSupportedHapticEffects,
104 can_gc: CanGc,
105 ) -> DomRoot<GamepadHapticActuator> {
106 reflect_dom_object(
107 Box::new(GamepadHapticActuator::new_inherited(
108 gamepad_index,
109 supported_haptic_effects,
110 )),
111 window,
112 can_gc,
113 )
114 }
115}
116
117impl GamepadHapticActuatorMethods<crate::DomTypeHolder> for GamepadHapticActuator {
118 fn Effects(&self, cx: JSContext, can_gc: CanGc, retval: MutableHandleValue) {
120 to_frozen_array(self.effects.as_slice(), cx, retval, can_gc)
121 }
122
123 fn PlayEffect(
125 &self,
126 type_: GamepadHapticEffectType,
127 params: &GamepadEffectParameters,
128 comp: InRealm,
129 can_gc: CanGc,
130 ) -> Rc<Promise> {
131 let playing_effect_promise = Promise::new_in_current_realm(comp, can_gc);
132
133 match type_ {
135 GamepadHapticEffectType::Dual_rumble => {
137 if *params.strongMagnitude < 0.0 || *params.strongMagnitude > 1.0 {
138 playing_effect_promise.reject_error(
139 Error::Type(
140 "Strong magnitude value is not within range of 0.0 to 1.0.".to_string(),
141 ),
142 can_gc,
143 );
144 return playing_effect_promise;
145 } else if *params.weakMagnitude < 0.0 || *params.weakMagnitude > 1.0 {
146 playing_effect_promise.reject_error(
147 Error::Type(
148 "Weak magnitude value is not within range of 0.0 to 1.0.".to_string(),
149 ),
150 can_gc,
151 );
152 return playing_effect_promise;
153 }
154 },
155 GamepadHapticEffectType::Trigger_rumble => {
157 if *params.strongMagnitude < 0.0 || *params.strongMagnitude > 1.0 {
158 playing_effect_promise.reject_error(
159 Error::Type(
160 "Strong magnitude value is not within range of 0.0 to 1.0.".to_string(),
161 ),
162 can_gc,
163 );
164 return playing_effect_promise;
165 } else if *params.weakMagnitude < 0.0 || *params.weakMagnitude > 1.0 {
166 playing_effect_promise.reject_error(
167 Error::Type(
168 "Weak magnitude value is not within range of 0.0 to 1.0.".to_string(),
169 ),
170 can_gc,
171 );
172 return playing_effect_promise;
173 } else if *params.leftTrigger < 0.0 || *params.leftTrigger > 1.0 {
174 playing_effect_promise.reject_error(
175 Error::Type(
176 "Left trigger value is not within range of 0.0 to 1.0.".to_string(),
177 ),
178 can_gc,
179 );
180 return playing_effect_promise;
181 } else if *params.rightTrigger < 0.0 || *params.rightTrigger > 1.0 {
182 playing_effect_promise.reject_error(
183 Error::Type(
184 "Right trigger value is not within range of 0.0 to 1.0.".to_string(),
185 ),
186 can_gc,
187 );
188 return playing_effect_promise;
189 }
190 },
191 }
192
193 let document = self.global().as_window().Document();
194 if !document.is_fully_active() {
195 playing_effect_promise.reject_error(Error::InvalidState, can_gc);
196 }
197
198 self.sequence_id.set(self.sequence_id.get().wrapping_add(1));
199
200 if let Some(promise) = self.playing_effect_promise.borrow_mut().take() {
201 let trusted_promise = TrustedPromise::new(promise);
202 self.global().task_manager().gamepad_task_source().queue(
203 task!(preempt_promise: move || {
204 let promise = trusted_promise.root();
205 let message = DOMString::from("preempted");
206 promise.resolve_native(&message, CanGc::note());
207 }),
208 );
209 }
210
211 if !self.effects.contains(&type_) {
212 playing_effect_promise.reject_error(Error::NotSupported, can_gc);
213 return playing_effect_promise;
214 }
215
216 *self.playing_effect_promise.borrow_mut() = Some(playing_effect_promise.clone());
217 self.effect_sequence_id.set(self.sequence_id.get());
218
219 let context = Trusted::new(self);
220 let (effect_complete_sender, effect_complete_receiver) =
221 ipc::channel().expect("ipc channel failure");
222 let listener = HapticEffectListener {
223 task_source: self.global().task_manager().gamepad_task_source().into(),
224 context,
225 };
226
227 ROUTER.add_typed_route(
228 effect_complete_receiver,
229 Box::new(move |message| match message {
230 Ok(msg) => listener.handle_completed(msg),
231 Err(err) => warn!("Error receiving a GamepadMsg: {:?}", err),
232 }),
233 );
234
235 let params = DualRumbleEffectParams {
240 duration: params.duration as f64,
241 start_delay: params.startDelay as f64,
242 strong_magnitude: *params.strongMagnitude,
243 weak_magnitude: *params.weakMagnitude,
244 };
245 let event = EmbedderMsg::PlayGamepadHapticEffect(
246 document.webview_id(),
247 self.gamepad_index as usize,
248 embedder_traits::GamepadHapticEffectType::DualRumble(params),
249 effect_complete_sender,
250 );
251 self.global().as_window().send_to_embedder(event);
252
253 playing_effect_promise
254 }
255
256 fn Reset(&self, comp: InRealm, can_gc: CanGc) -> Rc<Promise> {
258 let promise = Promise::new_in_current_realm(comp, can_gc);
259
260 let document = self.global().as_window().Document();
261 if !document.is_fully_active() {
262 promise.reject_error(Error::InvalidState, can_gc);
263 return promise;
264 }
265
266 self.sequence_id.set(self.sequence_id.get().wrapping_add(1));
267
268 if let Some(promise) = self.playing_effect_promise.borrow_mut().take() {
269 let trusted_promise = TrustedPromise::new(promise);
270 self.global().task_manager().gamepad_task_source().queue(
271 task!(preempt_promise: move || {
272 let promise = trusted_promise.root();
273 let message = DOMString::from("preempted");
274 promise.resolve_native(&message, CanGc::note());
275 }),
276 );
277 }
278
279 *self.playing_effect_promise.borrow_mut() = Some(promise.clone());
280
281 self.reset_sequence_id.set(self.sequence_id.get());
282
283 let context = Trusted::new(self);
284 let (effect_stop_sender, effect_stop_receiver) =
285 ipc::channel().expect("ipc channel failure");
286 let listener = HapticEffectListener {
287 task_source: self.global().task_manager().gamepad_task_source().into(),
288 context,
289 };
290
291 ROUTER.add_typed_route(
292 effect_stop_receiver,
293 Box::new(move |message| match message {
294 Ok(msg) => listener.handle_stopped(msg),
295 Err(err) => warn!("Error receiving a GamepadMsg: {:?}", err),
296 }),
297 );
298
299 let event = EmbedderMsg::StopGamepadHapticEffect(
300 document.webview_id(),
301 self.gamepad_index as usize,
302 effect_stop_sender,
303 );
304 self.global().as_window().send_to_embedder(event);
305
306 self.playing_effect_promise.borrow().clone().unwrap()
307 }
308}
309
310impl GamepadHapticActuator {
311 pub(crate) fn handle_haptic_effect_completed(
314 &self,
315 completed_successfully: bool,
316 can_gc: CanGc,
317 ) {
318 if self.effect_sequence_id.get() != self.sequence_id.get() || !completed_successfully {
319 return;
320 }
321 let playing_effect_promise = self.playing_effect_promise.borrow_mut().take();
322 if let Some(promise) = playing_effect_promise {
323 let message = DOMString::from("complete");
324 promise.resolve_native(&message, can_gc);
325 }
326 }
327
328 pub(crate) fn handle_haptic_effect_stopped(&self, stopped_successfully: bool) {
331 if !stopped_successfully {
332 return;
333 }
334
335 let playing_effect_promise = self.playing_effect_promise.borrow_mut().take();
336
337 if let Some(promise) = playing_effect_promise {
338 let trusted_promise = TrustedPromise::new(promise);
339 let sequence_id = self.sequence_id.get();
340 let reset_sequence_id = self.reset_sequence_id.get();
341 self.global().task_manager().gamepad_task_source().queue(
342 task!(complete_promise: move || {
343 if sequence_id != reset_sequence_id {
344 warn!("Mismatched sequence/reset sequence ids: {} != {}", sequence_id, reset_sequence_id);
345 return;
346 }
347 let promise = trusted_promise.root();
348 let message = DOMString::from("complete");
349 promise.resolve_native(&message, CanGc::note());
350 })
351 );
352 }
353 }
354
355 pub(crate) fn handle_visibility_change(&self) {
357 if self.playing_effect_promise.borrow().is_none() {
358 return;
359 }
360
361 let this = Trusted::new(self);
362 self.global().task_manager().gamepad_task_source().queue(
363 task!(stop_playing_effect: move || {
364 let actuator = this.root();
365 let Some(promise) = actuator.playing_effect_promise.borrow_mut().take() else {
366 return;
367 };
368 let message = DOMString::from("preempted");
369 promise.resolve_native(&message, CanGc::note());
370 }),
371 );
372
373 let (send, _rcv) = ipc::channel().expect("ipc channel failure");
374
375 let document = self.global().as_window().Document();
376 let event = EmbedderMsg::StopGamepadHapticEffect(
377 document.webview_id(),
378 self.gamepad_index as usize,
379 send,
380 );
381 self.global().as_window().send_to_embedder(event);
382 }
383}