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