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