Skip to main content

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