script/dom/gamepad/
gamepadhapticactuator.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::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/// <https://www.w3.org/TR/gamepad/#gamepadhapticactuator-interface>
56#[dom_struct]
57pub(crate) struct GamepadHapticActuator {
58    reflector_: Reflector,
59    gamepad_index: u32,
60    /// <https://www.w3.org/TR/gamepad/#dfn-effects>
61    effects: Vec<GamepadHapticEffectType>,
62    /// <https://www.w3.org/TR/gamepad/#dfn-playingeffectpromise>
63    #[conditional_malloc_size_of]
64    playing_effect_promise: DomRefCell<Option<Rc<Promise>>>,
65    /// The current sequence ID for playing effects,
66    /// incremented on every call to playEffect() or reset().
67    /// Used to ensure that promises are resolved correctly.
68    /// Based on this pending PR <https://github.com/w3c/gamepad/pull/201>
69    sequence_id: Cell<u32>,
70    /// The sequence ID during the last playEffect() call
71    effect_sequence_id: Cell<u32>,
72    /// The sequence ID during the last reset() call
73    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    /// <https://www.w3.org/TR/gamepad/#dom-gamepadhapticactuator-effects>
118    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    /// <https://www.w3.org/TR/gamepad/#dom-gamepadhapticactuator-playeffect>
123    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        // <https://www.w3.org/TR/gamepad/#dfn-valid-effect>
133        match type_ {
134            // <https://www.w3.org/TR/gamepad/#dfn-valid-dual-rumble-effect>
135            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            // <https://www.w3.org/TR/gamepad/#dfn-valid-trigger-rumble-effect>
155            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        // Note: The spec says we SHOULD also pass a playEffectTimestamp for more precise playback timing
231        // when start_delay is non-zero, but this is left more as a footnote without much elaboration.
232        // <https://www.w3.org/TR/gamepad/#dfn-issue-a-haptic-effect>
233
234        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    /// <https://www.w3.org/TR/gamepad/#dom-gamepadhapticactuator-reset>
252    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    /// <https://www.w3.org/TR/gamepad/#dom-gamepadhapticactuator-playeffect>
303    /// We are in the task queued by the "in-parallel" steps.
304    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    /// <https://www.w3.org/TR/gamepad/#dom-gamepadhapticactuator-reset>
320    /// We are in the task queued by the "in-parallel" steps.
321    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    /// <https://www.w3.org/TR/gamepad/#handling-visibility-change>
347    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}