script/dom/gamepad/
gamepad.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::{GamepadSupportedHapticEffects, GamepadUpdateType};
9use js::typedarray::{Float64, Float64Array};
10
11use super::gamepadbuttonlist::GamepadButtonList;
12use super::gamepadhapticactuator::GamepadHapticActuator;
13use super::gamepadpose::GamepadPose;
14use crate::dom::bindings::buffer_source::HeapBufferSource;
15use crate::dom::bindings::codegen::Bindings::GamepadBinding::{GamepadHand, GamepadMethods};
16use crate::dom::bindings::codegen::Bindings::GamepadButtonListBinding::GamepadButtonListMethods;
17use crate::dom::bindings::inheritance::Castable;
18use crate::dom::bindings::num::Finite;
19use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object};
20use crate::dom::bindings::root::{Dom, DomRoot};
21use crate::dom::bindings::str::DOMString;
22use crate::dom::event::Event;
23use crate::dom::eventtarget::EventTarget;
24use crate::dom::gamepadevent::{GamepadEvent, GamepadEventType};
25use crate::dom::globalscope::GlobalScope;
26use crate::dom::window::Window;
27use crate::script_runtime::{CanGc, JSContext};
28
29// This value is for determining when to consider a gamepad as having a user gesture
30// from an axis tilt. This matches the threshold in Chromium.
31const AXIS_TILT_THRESHOLD: f64 = 0.5;
32// This value is for determining when to consider a non-digital button "pressed".
33// Like Gecko and Chromium it derives from the XInput trigger threshold.
34const BUTTON_PRESS_THRESHOLD: f64 = 30.0 / 255.0;
35
36#[dom_struct]
37pub(crate) struct Gamepad {
38    reflector_: Reflector,
39    gamepad_id: u32,
40    id: String,
41    index: Cell<i32>,
42    connected: Cell<bool>,
43    timestamp: Cell<f64>,
44    mapping_type: String,
45    #[ignore_malloc_size_of = "mozjs"]
46    axes: HeapBufferSource<Float64>,
47    buttons: Dom<GamepadButtonList>,
48    pose: Option<Dom<GamepadPose>>,
49    #[ignore_malloc_size_of = "Defined in rust-webvr"]
50    hand: GamepadHand,
51    axis_bounds: (f64, f64),
52    button_bounds: (f64, f64),
53    exposed: Cell<bool>,
54    vibration_actuator: Dom<GamepadHapticActuator>,
55}
56
57impl Gamepad {
58    #[allow(clippy::too_many_arguments)]
59    fn new_inherited(
60        gamepad_id: u32,
61        id: String,
62        index: i32,
63        connected: bool,
64        timestamp: f64,
65        mapping_type: String,
66        buttons: &GamepadButtonList,
67        pose: Option<&GamepadPose>,
68        hand: GamepadHand,
69        axis_bounds: (f64, f64),
70        button_bounds: (f64, f64),
71        vibration_actuator: &GamepadHapticActuator,
72    ) -> Gamepad {
73        Self {
74            reflector_: Reflector::new(),
75            gamepad_id,
76            id,
77            index: Cell::new(index),
78            connected: Cell::new(connected),
79            timestamp: Cell::new(timestamp),
80            mapping_type,
81            axes: HeapBufferSource::default(),
82            buttons: Dom::from_ref(buttons),
83            pose: pose.map(Dom::from_ref),
84            hand,
85            axis_bounds,
86            button_bounds,
87            exposed: Cell::new(false),
88            vibration_actuator: Dom::from_ref(vibration_actuator),
89        }
90    }
91
92    /// When we construct a new gamepad, we initialize the number of buttons and
93    /// axes corresponding to the "standard" gamepad mapping.
94    /// The spec says UAs *may* do this for fingerprint mitigation, and it also
95    /// happens to simplify implementation
96    /// <https://www.w3.org/TR/gamepad/#fingerprinting-mitigation>
97    #[allow(clippy::too_many_arguments)]
98    pub(crate) fn new(
99        window: &Window,
100        gamepad_id: u32,
101        id: String,
102        mapping_type: String,
103        axis_bounds: (f64, f64),
104        button_bounds: (f64, f64),
105        supported_haptic_effects: GamepadSupportedHapticEffects,
106        xr: bool,
107        can_gc: CanGc,
108    ) -> DomRoot<Gamepad> {
109        let button_list = GamepadButtonList::init_buttons(window, can_gc);
110        let vibration_actuator =
111            GamepadHapticActuator::new(window, gamepad_id, supported_haptic_effects, can_gc);
112        let index = if xr { -1 } else { 0 };
113        let gamepad = reflect_dom_object(
114            Box::new(Gamepad::new_inherited(
115                gamepad_id,
116                id,
117                index,
118                true,
119                0.,
120                mapping_type,
121                &button_list,
122                None,
123                GamepadHand::_empty,
124                axis_bounds,
125                button_bounds,
126                &vibration_actuator,
127            )),
128            window,
129            can_gc,
130        );
131        gamepad.init_axes(can_gc);
132        gamepad
133    }
134}
135
136impl GamepadMethods<crate::DomTypeHolder> for Gamepad {
137    // https://w3c.github.io/gamepad/#dom-gamepad-id
138    fn Id(&self) -> DOMString {
139        DOMString::from(self.id.clone())
140    }
141
142    // https://w3c.github.io/gamepad/#dom-gamepad-index
143    fn Index(&self) -> i32 {
144        self.index.get()
145    }
146
147    // https://w3c.github.io/gamepad/#dom-gamepad-connected
148    fn Connected(&self) -> bool {
149        self.connected.get()
150    }
151
152    // https://w3c.github.io/gamepad/#dom-gamepad-timestamp
153    fn Timestamp(&self) -> Finite<f64> {
154        Finite::wrap(self.timestamp.get())
155    }
156
157    // https://w3c.github.io/gamepad/#dom-gamepad-mapping
158    fn Mapping(&self) -> DOMString {
159        DOMString::from(self.mapping_type.clone())
160    }
161
162    // https://w3c.github.io/gamepad/#dom-gamepad-axes
163    fn Axes(&self, _cx: JSContext) -> Float64Array {
164        self.axes
165            .get_typed_array()
166            .expect("Failed to get gamepad axes.")
167    }
168
169    // https://w3c.github.io/gamepad/#dom-gamepad-buttons
170    fn Buttons(&self) -> DomRoot<GamepadButtonList> {
171        DomRoot::from_ref(&*self.buttons)
172    }
173
174    // https://w3c.github.io/gamepad/#dom-gamepad-vibrationactuator
175    fn VibrationActuator(&self) -> DomRoot<GamepadHapticActuator> {
176        DomRoot::from_ref(&*self.vibration_actuator)
177    }
178
179    // https://w3c.github.io/gamepad/extensions.html#gamepadhand-enum
180    fn Hand(&self) -> GamepadHand {
181        self.hand
182    }
183
184    // https://w3c.github.io/gamepad/extensions.html#dom-gamepad-pose
185    fn GetPose(&self) -> Option<DomRoot<GamepadPose>> {
186        self.pose.as_ref().map(|p| DomRoot::from_ref(&**p))
187    }
188}
189
190#[allow(dead_code)]
191impl Gamepad {
192    pub(crate) fn gamepad_id(&self) -> u32 {
193        self.gamepad_id
194    }
195
196    pub(crate) fn update_connected(&self, connected: bool, has_gesture: bool, can_gc: CanGc) {
197        if self.connected.get() == connected {
198            return;
199        }
200        self.connected.set(connected);
201
202        let event_type = if connected {
203            GamepadEventType::Connected
204        } else {
205            GamepadEventType::Disconnected
206        };
207
208        if has_gesture {
209            self.notify_event(event_type, can_gc);
210        }
211    }
212
213    pub(crate) fn index(&self) -> i32 {
214        self.index.get()
215    }
216
217    pub(crate) fn update_index(&self, index: i32) {
218        self.index.set(index);
219    }
220
221    pub(crate) fn update_timestamp(&self, timestamp: f64) {
222        self.timestamp.set(timestamp);
223    }
224
225    pub(crate) fn notify_event(&self, event_type: GamepadEventType, can_gc: CanGc) {
226        let event =
227            GamepadEvent::new_with_type(self.global().as_window(), event_type, self, can_gc);
228        event
229            .upcast::<Event>()
230            .fire(self.global().as_window().upcast::<EventTarget>(), can_gc);
231    }
232
233    /// Initialize the number of axes in the "standard" gamepad mapping.
234    /// <https://www.w3.org/TR/gamepad/#dfn-initializing-axes>
235    fn init_axes(&self, can_gc: CanGc) {
236        let initial_axes: Vec<f64> = vec![
237            0., // Horizontal axis for left stick (negative left/positive right)
238            0., // Vertical axis for left stick (negative up/positive down)
239            0., // Horizontal axis for right stick (negative left/positive right)
240            0., // Vertical axis for right stick (negative up/positive down)
241        ];
242        self.axes
243            .set_data(GlobalScope::get_cx(), &initial_axes, can_gc)
244            .expect("Failed to set axes data on gamepad.")
245    }
246
247    #[allow(unsafe_code)]
248    /// <https://www.w3.org/TR/gamepad/#dfn-map-and-normalize-axes>
249    pub(crate) fn map_and_normalize_axes(&self, axis_index: usize, value: f64) {
250        // Let normalizedValue be 2 (logicalValue − logicalMinimum) / (logicalMaximum − logicalMinimum) − 1.
251        let numerator = value - self.axis_bounds.0;
252        let denominator = self.axis_bounds.1 - self.axis_bounds.0;
253        if denominator != 0.0 && denominator.is_finite() {
254            let normalized_value: f64 = 2.0 * numerator / denominator - 1.0;
255            if normalized_value.is_finite() {
256                let mut axis_vec = self
257                    .axes
258                    .typed_array_to_option()
259                    .expect("Axes have not been initialized!");
260                unsafe {
261                    axis_vec.as_mut_slice()[axis_index] = normalized_value;
262                }
263            } else {
264                warn!("Axis value is not finite!");
265            }
266        } else {
267            warn!("Axis bounds difference is either 0 or non-finite!");
268        }
269    }
270
271    /// <https://www.w3.org/TR/gamepad/#dfn-map-and-normalize-buttons>
272    pub(crate) fn map_and_normalize_buttons(&self, button_index: usize, value: f64) {
273        // Let normalizedValue be (logicalValue − logicalMinimum) / (logicalMaximum − logicalMinimum).
274        let numerator = value - self.button_bounds.0;
275        let denominator = self.button_bounds.1 - self.button_bounds.0;
276        if denominator != 0.0 && denominator.is_finite() {
277            let normalized_value: f64 = numerator / denominator;
278            if normalized_value.is_finite() {
279                let pressed = normalized_value >= BUTTON_PRESS_THRESHOLD;
280                // TODO: Determine a way of getting touch capability for button
281                if let Some(button) = self.buttons.IndexedGetter(button_index as u32) {
282                    button.update(pressed, /*touched*/ pressed, normalized_value);
283                }
284            } else {
285                warn!("Button value is not finite!");
286            }
287        } else {
288            warn!("Button bounds difference is either 0 or non-finite!");
289        }
290    }
291
292    /// <https://www.w3.org/TR/gamepad/#dfn-exposed>
293    pub(crate) fn exposed(&self) -> bool {
294        self.exposed.get()
295    }
296
297    /// <https://www.w3.org/TR/gamepad/#dfn-exposed>
298    pub(crate) fn set_exposed(&self, exposed: bool) {
299        self.exposed.set(exposed);
300    }
301
302    pub(crate) fn vibration_actuator(&self) -> &GamepadHapticActuator {
303        &self.vibration_actuator
304    }
305}
306
307/// <https://www.w3.org/TR/gamepad/#dfn-gamepad-user-gesture>
308pub(crate) fn contains_user_gesture(update_type: GamepadUpdateType) -> bool {
309    match update_type {
310        GamepadUpdateType::Axis(_, value) => value.abs() > AXIS_TILT_THRESHOLD,
311        GamepadUpdateType::Button(_, value) => value > BUTTON_PRESS_THRESHOLD,
312    }
313}