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