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::{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    /// When we construct a new gamepad, we initialize the number of buttons and
101    /// axes corresponding to the "standard" gamepad mapping.
102    /// The spec says UAs *may* do this for fingerprint mitigation, and it also
103    /// happens to simplify implementation
104    /// <https://www.w3.org/TR/gamepad/#fingerprinting-mitigation>
105    #[allow(clippy::too_many_arguments)]
106    pub(crate) fn new(
107        window: &Window,
108        gamepad_id: u32,
109        id: String,
110        mapping_type: String,
111        axis_bounds: (f64, f64),
112        button_bounds: (f64, f64),
113        supported_haptic_effects: GamepadSupportedHapticEffects,
114        xr: bool,
115        can_gc: CanGc,
116    ) -> DomRoot<Gamepad> {
117        let buttons = Gamepad::init_buttons(window, can_gc);
118        rooted_vec!(let buttons <- buttons.iter().map(DomRoot::as_traced));
119        let vibration_actuator =
120            GamepadHapticActuator::new(window, gamepad_id, supported_haptic_effects, can_gc);
121        let index = if xr { -1 } else { 0 };
122        let gamepad = reflect_dom_object(
123            Box::new(Gamepad::new_inherited(
124                gamepad_id,
125                id,
126                index,
127                true,
128                0.,
129                mapping_type,
130                buttons.r(),
131                None,
132                GamepadHand::_empty,
133                axis_bounds,
134                button_bounds,
135                &vibration_actuator,
136            )),
137            window,
138            can_gc,
139        );
140        gamepad.init_axes();
141        gamepad
142    }
143}
144
145impl GamepadMethods<crate::DomTypeHolder> for Gamepad {
146    /// <https://w3c.github.io/gamepad/#dom-gamepad-id>
147    fn Id(&self) -> DOMString {
148        DOMString::from(self.id.clone())
149    }
150
151    /// <https://w3c.github.io/gamepad/#dom-gamepad-index>
152    fn Index(&self) -> i32 {
153        self.index.get()
154    }
155
156    /// <https://w3c.github.io/gamepad/#dom-gamepad-connected>
157    fn Connected(&self) -> bool {
158        self.connected.get()
159    }
160
161    /// <https://w3c.github.io/gamepad/#dom-gamepad-timestamp>
162    fn Timestamp(&self) -> Finite<f64> {
163        Finite::wrap(self.timestamp.get())
164    }
165
166    /// <https://w3c.github.io/gamepad/#dom-gamepad-mapping>
167    fn Mapping(&self) -> DOMString {
168        DOMString::from(self.mapping_type.clone())
169    }
170
171    /// <https://w3c.github.io/gamepad/#dom-gamepad-axes>
172    fn Axes(&self, cx: &mut JSContext, retval: MutableHandleValue) {
173        self.frozen_axes.get_or_init(
174            || self.axes.borrow().clone(),
175            cx.into(),
176            retval,
177            CanGc::from_cx(cx),
178        );
179    }
180
181    /// <https://w3c.github.io/gamepad/#dom-gamepad-buttons>
182    fn Buttons(&self, cx: &mut JSContext, retval: MutableHandleValue) {
183        self.frozen_buttons.get_or_init(
184            || {
185                self.buttons
186                    .iter()
187                    .map(|b| DomRoot::from_ref(&**b))
188                    .collect()
189            },
190            cx.into(),
191            retval,
192            CanGc::from_cx(cx),
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(window: &Window, can_gc: CanGc) -> Vec<DomRoot<GamepadButton>> {
221        vec![
222            GamepadButton::new(window, false, false, can_gc), // Bottom button in right cluster
223            GamepadButton::new(window, false, false, can_gc), // Right button in right cluster
224            GamepadButton::new(window, false, false, can_gc), // Left button in right cluster
225            GamepadButton::new(window, false, false, can_gc), // Top button in right cluster
226            GamepadButton::new(window, false, false, can_gc), // Top left front button
227            GamepadButton::new(window, false, false, can_gc), // Top right front button
228            GamepadButton::new(window, false, false, can_gc), // Bottom left front button
229            GamepadButton::new(window, false, false, can_gc), // Bottom right front button
230            GamepadButton::new(window, false, false, can_gc), // Left button in center cluster
231            GamepadButton::new(window, false, false, can_gc), // Right button in center cluster
232            GamepadButton::new(window, false, false, can_gc), // Left stick pressed button
233            GamepadButton::new(window, false, false, can_gc), // Right stick pressed button
234            GamepadButton::new(window, false, false, can_gc), // Top button in left cluster
235            GamepadButton::new(window, false, false, can_gc), // Bottom button in left cluster
236            GamepadButton::new(window, false, false, can_gc), // Left button in left cluster
237            GamepadButton::new(window, false, false, can_gc), // Right button in left cluster
238            GamepadButton::new(window, false, false, can_gc), // Center button in center cluster
239        ]
240    }
241
242    pub(crate) fn update_connected(&self, connected: bool, has_gesture: bool, can_gc: CanGc) {
243        if self.connected.get() == connected {
244            return;
245        }
246        self.connected.set(connected);
247
248        let event_type = if connected {
249            GamepadEventType::Connected
250        } else {
251            GamepadEventType::Disconnected
252        };
253
254        if has_gesture {
255            self.notify_event(event_type, can_gc);
256        }
257    }
258
259    pub(crate) fn index(&self) -> i32 {
260        self.index.get()
261    }
262
263    pub(crate) fn update_index(&self, index: i32) {
264        self.index.set(index);
265    }
266
267    pub(crate) fn update_timestamp(&self, timestamp: f64) {
268        self.timestamp.set(timestamp);
269    }
270
271    pub(crate) fn notify_event(&self, event_type: GamepadEventType, can_gc: CanGc) {
272        let event =
273            GamepadEvent::new_with_type(self.global().as_window(), event_type, self, can_gc);
274        event
275            .upcast::<Event>()
276            .fire(self.global().as_window().upcast::<EventTarget>(), can_gc);
277    }
278
279    /// Initialize the number of axes in the "standard" gamepad mapping.
280    /// <https://www.w3.org/TR/gamepad/#dfn-initializing-axes>
281    fn init_axes(&self) {
282        *self.axes.borrow_mut() = vec![
283            0., // Horizontal axis for left stick (negative left/positive right)
284            0., // Vertical axis for left stick (negative up/positive down)
285            0., // Horizontal axis for right stick (negative left/positive right)
286            0., // Vertical axis for right stick (negative up/positive down)
287        ];
288    }
289
290    /// <https://www.w3.org/TR/gamepad/#dfn-map-and-normalize-axes>
291    pub(crate) fn map_and_normalize_axes(&self, axis_index: usize, value: f64) {
292        // Let normalizedValue be 2 (logicalValue − logicalMinimum) / (logicalMaximum − logicalMinimum) − 1.
293        let numerator = value - self.axis_bounds.0;
294        let denominator = self.axis_bounds.1 - self.axis_bounds.0;
295        if denominator != 0.0 && denominator.is_finite() {
296            let normalized_value: f64 = 2.0 * numerator / denominator - 1.0;
297            if normalized_value.is_finite() {
298                self.axes.borrow_mut()[axis_index] = normalized_value;
299                self.frozen_axes.clear();
300            } else {
301                warn!("Axis value is not finite!");
302            }
303        } else {
304            warn!("Axis bounds difference is either 0 or non-finite!");
305        }
306    }
307
308    /// <https://www.w3.org/TR/gamepad/#dfn-map-and-normalize-buttons>
309    pub(crate) fn map_and_normalize_buttons(&self, button_index: usize, value: f64) {
310        // Let normalizedValue be (logicalValue − logicalMinimum) / (logicalMaximum − logicalMinimum).
311        let numerator = value - self.button_bounds.0;
312        let denominator = self.button_bounds.1 - self.button_bounds.0;
313        if denominator != 0.0 && denominator.is_finite() {
314            let normalized_value: f64 = numerator / denominator;
315            if normalized_value.is_finite() {
316                let pressed = normalized_value >= BUTTON_PRESS_THRESHOLD;
317                // TODO: Determine a way of getting touch capability for button
318                if let Some(button) = self.buttons.get(button_index) {
319                    button.update(pressed, /*touched*/ pressed, normalized_value);
320                    self.frozen_buttons.clear();
321                }
322            } else {
323                warn!("Button value is not finite!");
324            }
325        } else {
326            warn!("Button bounds difference is either 0 or non-finite!");
327        }
328    }
329
330    /// <https://www.w3.org/TR/gamepad/#dfn-exposed>
331    pub(crate) fn exposed(&self) -> bool {
332        self.exposed.get()
333    }
334
335    /// <https://www.w3.org/TR/gamepad/#dfn-exposed>
336    pub(crate) fn set_exposed(&self, exposed: bool) {
337        self.exposed.set(exposed);
338    }
339
340    pub(crate) fn vibration_actuator(&self) -> &GamepadHapticActuator {
341        &self.vibration_actuator
342    }
343}
344
345/// <https://www.w3.org/TR/gamepad/#dfn-gamepad-user-gesture>
346pub(crate) fn contains_user_gesture(update_type: GamepadUpdateType) -> bool {
347    match update_type {
348        GamepadUpdateType::Axis(_, value) => value.abs() > AXIS_TILT_THRESHOLD,
349        GamepadUpdateType::Button(_, value) => value > BUTTON_PRESS_THRESHOLD,
350    }
351}