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;
6
7use dom_struct::dom_struct;
8use embedder_traits::{GamepadSupportedHapticEffects, GamepadUpdateType};
9use js::typedarray::{Float64, HeapFloat64Array};
10use script_bindings::trace::RootedTraceableBox;
11
12use super::gamepadbuttonlist::GamepadButtonList;
13use super::gamepadhapticactuator::GamepadHapticActuator;
14use super::gamepadpose::GamepadPose;
15use crate::dom::bindings::buffer_source::HeapBufferSource;
16use crate::dom::bindings::codegen::Bindings::GamepadBinding::{GamepadHand, GamepadMethods};
17use crate::dom::bindings::codegen::Bindings::GamepadButtonListBinding::GamepadButtonListMethods;
18use crate::dom::bindings::inheritance::Castable;
19use crate::dom::bindings::num::Finite;
20use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object};
21use crate::dom::bindings::root::{Dom, DomRoot};
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::globalscope::GlobalScope;
27use crate::dom::window::Window;
28use crate::script_runtime::{CanGc, JSContext};
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: String,
46    #[ignore_malloc_size_of = "mozjs"]
47    axes: HeapBufferSource<Float64>,
48    buttons: Dom<GamepadButtonList>,
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: &GamepadButtonList,
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            axes: HeapBufferSource::default(),
83            buttons: Dom::from_ref(buttons),
84            pose: pose.map(Dom::from_ref),
85            hand,
86            axis_bounds,
87            button_bounds,
88            exposed: Cell::new(false),
89            vibration_actuator: Dom::from_ref(vibration_actuator),
90        }
91    }
92
93    /// When we construct a new gamepad, we initialize the number of buttons and
94    /// axes corresponding to the "standard" gamepad mapping.
95    /// The spec says UAs *may* do this for fingerprint mitigation, and it also
96    /// happens to simplify implementation
97    /// <https://www.w3.org/TR/gamepad/#fingerprinting-mitigation>
98    #[allow(clippy::too_many_arguments)]
99    pub(crate) fn new(
100        window: &Window,
101        gamepad_id: u32,
102        id: String,
103        mapping_type: String,
104        axis_bounds: (f64, f64),
105        button_bounds: (f64, f64),
106        supported_haptic_effects: GamepadSupportedHapticEffects,
107        xr: bool,
108        can_gc: CanGc,
109    ) -> DomRoot<Gamepad> {
110        let button_list = GamepadButtonList::init_buttons(window, can_gc);
111        let vibration_actuator =
112            GamepadHapticActuator::new(window, gamepad_id, supported_haptic_effects, can_gc);
113        let index = if xr { -1 } else { 0 };
114        let gamepad = reflect_dom_object(
115            Box::new(Gamepad::new_inherited(
116                gamepad_id,
117                id,
118                index,
119                true,
120                0.,
121                mapping_type,
122                &button_list,
123                None,
124                GamepadHand::_empty,
125                axis_bounds,
126                button_bounds,
127                &vibration_actuator,
128            )),
129            window,
130            can_gc,
131        );
132        gamepad.init_axes(can_gc);
133        gamepad
134    }
135}
136
137impl GamepadMethods<crate::DomTypeHolder> for Gamepad {
138    /// <https://w3c.github.io/gamepad/#dom-gamepad-id>
139    fn Id(&self) -> DOMString {
140        DOMString::from(self.id.clone())
141    }
142
143    /// <https://w3c.github.io/gamepad/#dom-gamepad-index>
144    fn Index(&self) -> i32 {
145        self.index.get()
146    }
147
148    /// <https://w3c.github.io/gamepad/#dom-gamepad-connected>
149    fn Connected(&self) -> bool {
150        self.connected.get()
151    }
152
153    /// <https://w3c.github.io/gamepad/#dom-gamepad-timestamp>
154    fn Timestamp(&self) -> Finite<f64> {
155        Finite::wrap(self.timestamp.get())
156    }
157
158    /// <https://w3c.github.io/gamepad/#dom-gamepad-mapping>
159    fn Mapping(&self) -> DOMString {
160        DOMString::from(self.mapping_type.clone())
161    }
162
163    /// <https://w3c.github.io/gamepad/#dom-gamepad-axes>
164    fn Axes(&self, _cx: JSContext) -> RootedTraceableBox<HeapFloat64Array> {
165        self.axes
166            .get_typed_array()
167            .expect("Failed to get gamepad axes.")
168    }
169
170    /// <https://w3c.github.io/gamepad/#dom-gamepad-buttons>
171    fn Buttons(&self) -> DomRoot<GamepadButtonList> {
172        DomRoot::from_ref(&*self.buttons)
173    }
174
175    /// <https://w3c.github.io/gamepad/#dom-gamepad-vibrationactuator>
176    fn VibrationActuator(&self) -> DomRoot<GamepadHapticActuator> {
177        DomRoot::from_ref(&*self.vibration_actuator)
178    }
179
180    /// <https://w3c.github.io/gamepad/extensions.html#gamepadhand-enum>
181    fn Hand(&self) -> GamepadHand {
182        self.hand
183    }
184
185    /// <https://w3c.github.io/gamepad/extensions.html#dom-gamepad-pose>
186    fn GetPose(&self) -> Option<DomRoot<GamepadPose>> {
187        self.pose.as_ref().map(|p| DomRoot::from_ref(&**p))
188    }
189}
190
191#[expect(dead_code)]
192impl Gamepad {
193    pub(crate) fn gamepad_id(&self) -> u32 {
194        self.gamepad_id
195    }
196
197    pub(crate) fn update_connected(&self, connected: bool, has_gesture: bool, can_gc: CanGc) {
198        if self.connected.get() == connected {
199            return;
200        }
201        self.connected.set(connected);
202
203        let event_type = if connected {
204            GamepadEventType::Connected
205        } else {
206            GamepadEventType::Disconnected
207        };
208
209        if has_gesture {
210            self.notify_event(event_type, can_gc);
211        }
212    }
213
214    pub(crate) fn index(&self) -> i32 {
215        self.index.get()
216    }
217
218    pub(crate) fn update_index(&self, index: i32) {
219        self.index.set(index);
220    }
221
222    pub(crate) fn update_timestamp(&self, timestamp: f64) {
223        self.timestamp.set(timestamp);
224    }
225
226    pub(crate) fn notify_event(&self, event_type: GamepadEventType, can_gc: CanGc) {
227        let event =
228            GamepadEvent::new_with_type(self.global().as_window(), event_type, self, can_gc);
229        event
230            .upcast::<Event>()
231            .fire(self.global().as_window().upcast::<EventTarget>(), can_gc);
232    }
233
234    /// Initialize the number of axes in the "standard" gamepad mapping.
235    /// <https://www.w3.org/TR/gamepad/#dfn-initializing-axes>
236    fn init_axes(&self, can_gc: CanGc) {
237        let initial_axes: Vec<f64> = vec![
238            0., // Horizontal axis for left stick (negative left/positive right)
239            0., // Vertical axis for left stick (negative up/positive down)
240            0., // Horizontal axis for right stick (negative left/positive right)
241            0., // Vertical axis for right stick (negative up/positive down)
242        ];
243        self.axes
244            .set_data(GlobalScope::get_cx(), &initial_axes, can_gc)
245            .expect("Failed to set axes data on gamepad.")
246    }
247
248    #[expect(unsafe_code)]
249    /// <https://www.w3.org/TR/gamepad/#dfn-map-and-normalize-axes>
250    pub(crate) fn map_and_normalize_axes(&self, axis_index: usize, value: f64) {
251        // Let normalizedValue be 2 (logicalValue − logicalMinimum) / (logicalMaximum − logicalMinimum) − 1.
252        let numerator = value - self.axis_bounds.0;
253        let denominator = self.axis_bounds.1 - self.axis_bounds.0;
254        if denominator != 0.0 && denominator.is_finite() {
255            let normalized_value: f64 = 2.0 * numerator / denominator - 1.0;
256            if normalized_value.is_finite() {
257                let mut axis_vec = self
258                    .axes
259                    .typed_array_to_option()
260                    .expect("Axes have not been initialized!");
261                unsafe {
262                    axis_vec.as_mut_slice()[axis_index] = normalized_value;
263                }
264            } else {
265                warn!("Axis value is not finite!");
266            }
267        } else {
268            warn!("Axis bounds difference is either 0 or non-finite!");
269        }
270    }
271
272    /// <https://www.w3.org/TR/gamepad/#dfn-map-and-normalize-buttons>
273    pub(crate) fn map_and_normalize_buttons(&self, button_index: usize, value: f64) {
274        // Let normalizedValue be (logicalValue − logicalMinimum) / (logicalMaximum − logicalMinimum).
275        let numerator = value - self.button_bounds.0;
276        let denominator = self.button_bounds.1 - self.button_bounds.0;
277        if denominator != 0.0 && denominator.is_finite() {
278            let normalized_value: f64 = numerator / denominator;
279            if normalized_value.is_finite() {
280                let pressed = normalized_value >= BUTTON_PRESS_THRESHOLD;
281                // TODO: Determine a way of getting touch capability for button
282                if let Some(button) = self.buttons.IndexedGetter(button_index as u32) {
283                    button.update(pressed, /*touched*/ pressed, normalized_value);
284                }
285            } else {
286                warn!("Button value is not finite!");
287            }
288        } else {
289            warn!("Button bounds difference is either 0 or non-finite!");
290        }
291    }
292
293    /// <https://www.w3.org/TR/gamepad/#dfn-exposed>
294    pub(crate) fn exposed(&self) -> bool {
295        self.exposed.get()
296    }
297
298    /// <https://www.w3.org/TR/gamepad/#dfn-exposed>
299    pub(crate) fn set_exposed(&self, exposed: bool) {
300        self.exposed.set(exposed);
301    }
302
303    pub(crate) fn vibration_actuator(&self) -> &GamepadHapticActuator {
304        &self.vibration_actuator
305    }
306}
307
308/// <https://www.w3.org/TR/gamepad/#dfn-gamepad-user-gesture>
309pub(crate) fn contains_user_gesture(update_type: GamepadUpdateType) -> bool {
310    match update_type {
311        GamepadUpdateType::Axis(_, value) => value.abs() > AXIS_TILT_THRESHOLD,
312        GamepadUpdateType::Button(_, value) => value > BUTTON_PRESS_THRESHOLD,
313    }
314}