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