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