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