Skip to main content

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