script: Move gamepad DOM interfaces to script/dom/gamepad/ (#38900)

Moves interfaces defined by the gamepad spec to the
`script/dom/gamepad/` module from `script/dom/`.

Testing: Just a refactor shouldn't need any testing
Fixes: N/A

Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
This commit is contained in:
Ashwin Naren 2025-08-27 11:39:27 -07:00 committed by GitHub
parent 21a7782206
commit 461ff26812
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 26 additions and 18 deletions

View file

@ -0,0 +1,313 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::cell::Cell;
use dom_struct::dom_struct;
use embedder_traits::{GamepadSupportedHapticEffects, GamepadUpdateType};
use js::typedarray::{Float64, Float64Array};
use super::gamepadbuttonlist::GamepadButtonList;
use super::gamepadhapticactuator::GamepadHapticActuator;
use super::gamepadpose::GamepadPose;
use crate::dom::bindings::buffer_source::HeapBufferSource;
use crate::dom::bindings::codegen::Bindings::GamepadBinding::{GamepadHand, GamepadMethods};
use crate::dom::bindings::codegen::Bindings::GamepadButtonListBinding::GamepadButtonListMethods;
use crate::dom::bindings::inheritance::Castable;
use crate::dom::bindings::num::Finite;
use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object};
use crate::dom::bindings::root::{Dom, DomRoot};
use crate::dom::bindings::str::DOMString;
use crate::dom::event::Event;
use crate::dom::eventtarget::EventTarget;
use crate::dom::gamepadevent::{GamepadEvent, GamepadEventType};
use crate::dom::globalscope::GlobalScope;
use crate::dom::window::Window;
use crate::script_runtime::{CanGc, JSContext};
// This value is for determining when to consider a gamepad as having a user gesture
// from an axis tilt. This matches the threshold in Chromium.
const AXIS_TILT_THRESHOLD: f64 = 0.5;
// This value is for determining when to consider a non-digital button "pressed".
// Like Gecko and Chromium it derives from the XInput trigger threshold.
const BUTTON_PRESS_THRESHOLD: f64 = 30.0 / 255.0;
#[dom_struct]
pub(crate) struct Gamepad {
reflector_: Reflector,
gamepad_id: u32,
id: String,
index: Cell<i32>,
connected: Cell<bool>,
timestamp: Cell<f64>,
mapping_type: String,
#[ignore_malloc_size_of = "mozjs"]
axes: HeapBufferSource<Float64>,
buttons: Dom<GamepadButtonList>,
pose: Option<Dom<GamepadPose>>,
#[ignore_malloc_size_of = "Defined in rust-webvr"]
hand: GamepadHand,
axis_bounds: (f64, f64),
button_bounds: (f64, f64),
exposed: Cell<bool>,
vibration_actuator: Dom<GamepadHapticActuator>,
}
impl Gamepad {
#[allow(clippy::too_many_arguments)]
fn new_inherited(
gamepad_id: u32,
id: String,
index: i32,
connected: bool,
timestamp: f64,
mapping_type: String,
buttons: &GamepadButtonList,
pose: Option<&GamepadPose>,
hand: GamepadHand,
axis_bounds: (f64, f64),
button_bounds: (f64, f64),
vibration_actuator: &GamepadHapticActuator,
) -> Gamepad {
Self {
reflector_: Reflector::new(),
gamepad_id,
id,
index: Cell::new(index),
connected: Cell::new(connected),
timestamp: Cell::new(timestamp),
mapping_type,
axes: HeapBufferSource::default(),
buttons: Dom::from_ref(buttons),
pose: pose.map(Dom::from_ref),
hand,
axis_bounds,
button_bounds,
exposed: Cell::new(false),
vibration_actuator: Dom::from_ref(vibration_actuator),
}
}
/// When we construct a new gamepad, we initialize the number of buttons and
/// axes corresponding to the "standard" gamepad mapping.
/// The spec says UAs *may* do this for fingerprint mitigation, and it also
/// happens to simplify implementation
/// <https://www.w3.org/TR/gamepad/#fingerprinting-mitigation>
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
window: &Window,
gamepad_id: u32,
id: String,
mapping_type: String,
axis_bounds: (f64, f64),
button_bounds: (f64, f64),
supported_haptic_effects: GamepadSupportedHapticEffects,
xr: bool,
can_gc: CanGc,
) -> DomRoot<Gamepad> {
let button_list = GamepadButtonList::init_buttons(window, can_gc);
let vibration_actuator =
GamepadHapticActuator::new(window, gamepad_id, supported_haptic_effects, can_gc);
let index = if xr { -1 } else { 0 };
let gamepad = reflect_dom_object(
Box::new(Gamepad::new_inherited(
gamepad_id,
id,
index,
true,
0.,
mapping_type,
&button_list,
None,
GamepadHand::_empty,
axis_bounds,
button_bounds,
&vibration_actuator,
)),
window,
can_gc,
);
gamepad.init_axes(can_gc);
gamepad
}
}
impl GamepadMethods<crate::DomTypeHolder> for Gamepad {
// https://w3c.github.io/gamepad/#dom-gamepad-id
fn Id(&self) -> DOMString {
DOMString::from(self.id.clone())
}
// https://w3c.github.io/gamepad/#dom-gamepad-index
fn Index(&self) -> i32 {
self.index.get()
}
// https://w3c.github.io/gamepad/#dom-gamepad-connected
fn Connected(&self) -> bool {
self.connected.get()
}
// https://w3c.github.io/gamepad/#dom-gamepad-timestamp
fn Timestamp(&self) -> Finite<f64> {
Finite::wrap(self.timestamp.get())
}
// https://w3c.github.io/gamepad/#dom-gamepad-mapping
fn Mapping(&self) -> DOMString {
DOMString::from(self.mapping_type.clone())
}
// https://w3c.github.io/gamepad/#dom-gamepad-axes
fn Axes(&self, _cx: JSContext) -> Float64Array {
self.axes
.get_typed_array()
.expect("Failed to get gamepad axes.")
}
// https://w3c.github.io/gamepad/#dom-gamepad-buttons
fn Buttons(&self) -> DomRoot<GamepadButtonList> {
DomRoot::from_ref(&*self.buttons)
}
// https://w3c.github.io/gamepad/#dom-gamepad-vibrationactuator
fn VibrationActuator(&self) -> DomRoot<GamepadHapticActuator> {
DomRoot::from_ref(&*self.vibration_actuator)
}
// https://w3c.github.io/gamepad/extensions.html#gamepadhand-enum
fn Hand(&self) -> GamepadHand {
self.hand
}
// https://w3c.github.io/gamepad/extensions.html#dom-gamepad-pose
fn GetPose(&self) -> Option<DomRoot<GamepadPose>> {
self.pose.as_ref().map(|p| DomRoot::from_ref(&**p))
}
}
#[allow(dead_code)]
impl Gamepad {
pub(crate) fn gamepad_id(&self) -> u32 {
self.gamepad_id
}
pub(crate) fn update_connected(&self, connected: bool, has_gesture: bool, can_gc: CanGc) {
if self.connected.get() == connected {
return;
}
self.connected.set(connected);
let event_type = if connected {
GamepadEventType::Connected
} else {
GamepadEventType::Disconnected
};
if has_gesture {
self.notify_event(event_type, can_gc);
}
}
pub(crate) fn index(&self) -> i32 {
self.index.get()
}
pub(crate) fn update_index(&self, index: i32) {
self.index.set(index);
}
pub(crate) fn update_timestamp(&self, timestamp: f64) {
self.timestamp.set(timestamp);
}
pub(crate) fn notify_event(&self, event_type: GamepadEventType, can_gc: CanGc) {
let event =
GamepadEvent::new_with_type(self.global().as_window(), event_type, self, can_gc);
event
.upcast::<Event>()
.fire(self.global().as_window().upcast::<EventTarget>(), can_gc);
}
/// Initialize the number of axes in the "standard" gamepad mapping.
/// <https://www.w3.org/TR/gamepad/#dfn-initializing-axes>
fn init_axes(&self, can_gc: CanGc) {
let initial_axes: Vec<f64> = vec![
0., // Horizontal axis for left stick (negative left/positive right)
0., // Vertical axis for left stick (negative up/positive down)
0., // Horizontal axis for right stick (negative left/positive right)
0., // Vertical axis for right stick (negative up/positive down)
];
self.axes
.set_data(GlobalScope::get_cx(), &initial_axes, can_gc)
.expect("Failed to set axes data on gamepad.")
}
#[allow(unsafe_code)]
/// <https://www.w3.org/TR/gamepad/#dfn-map-and-normalize-axes>
pub(crate) fn map_and_normalize_axes(&self, axis_index: usize, value: f64) {
// Let normalizedValue be 2 (logicalValue logicalMinimum) / (logicalMaximum logicalMinimum) 1.
let numerator = value - self.axis_bounds.0;
let denominator = self.axis_bounds.1 - self.axis_bounds.0;
if denominator != 0.0 && denominator.is_finite() {
let normalized_value: f64 = 2.0 * numerator / denominator - 1.0;
if normalized_value.is_finite() {
let mut axis_vec = self
.axes
.typed_array_to_option()
.expect("Axes have not been initialized!");
unsafe {
axis_vec.as_mut_slice()[axis_index] = normalized_value;
}
} else {
warn!("Axis value is not finite!");
}
} else {
warn!("Axis bounds difference is either 0 or non-finite!");
}
}
/// <https://www.w3.org/TR/gamepad/#dfn-map-and-normalize-buttons>
pub(crate) fn map_and_normalize_buttons(&self, button_index: usize, value: f64) {
// Let normalizedValue be (logicalValue logicalMinimum) / (logicalMaximum logicalMinimum).
let numerator = value - self.button_bounds.0;
let denominator = self.button_bounds.1 - self.button_bounds.0;
if denominator != 0.0 && denominator.is_finite() {
let normalized_value: f64 = numerator / denominator;
if normalized_value.is_finite() {
let pressed = normalized_value >= BUTTON_PRESS_THRESHOLD;
// TODO: Determine a way of getting touch capability for button
if let Some(button) = self.buttons.IndexedGetter(button_index as u32) {
button.update(pressed, /*touched*/ pressed, normalized_value);
}
} else {
warn!("Button value is not finite!");
}
} else {
warn!("Button bounds difference is either 0 or non-finite!");
}
}
/// <https://www.w3.org/TR/gamepad/#dfn-exposed>
pub(crate) fn exposed(&self) -> bool {
self.exposed.get()
}
/// <https://www.w3.org/TR/gamepad/#dfn-exposed>
pub(crate) fn set_exposed(&self, exposed: bool) {
self.exposed.set(exposed);
}
pub(crate) fn vibration_actuator(&self) -> &GamepadHapticActuator {
&self.vibration_actuator
}
}
/// <https://www.w3.org/TR/gamepad/#dfn-gamepad-user-gesture>
pub(crate) fn contains_user_gesture(update_type: GamepadUpdateType) -> bool {
match update_type {
GamepadUpdateType::Axis(_, value) => value.abs() > AXIS_TILT_THRESHOLD,
GamepadUpdateType::Button(_, value) => value > BUTTON_PRESS_THRESHOLD,
}
}

View file

@ -0,0 +1,71 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::cell::Cell;
use dom_struct::dom_struct;
use crate::dom::bindings::codegen::Bindings::GamepadButtonBinding::GamepadButtonMethods;
use crate::dom::bindings::num::Finite;
use crate::dom::bindings::reflector::{Reflector, reflect_dom_object};
use crate::dom::bindings::root::DomRoot;
use crate::dom::window::Window;
use crate::script_runtime::CanGc;
#[dom_struct]
pub(crate) struct GamepadButton {
reflector_: Reflector,
pressed: Cell<bool>,
touched: Cell<bool>,
value: Cell<f64>,
}
impl GamepadButton {
pub(crate) fn new_inherited(pressed: bool, touched: bool) -> GamepadButton {
Self {
reflector_: Reflector::new(),
pressed: Cell::new(pressed),
touched: Cell::new(touched),
value: Cell::new(0.0),
}
}
pub(crate) fn new(
window: &Window,
pressed: bool,
touched: bool,
can_gc: CanGc,
) -> DomRoot<GamepadButton> {
reflect_dom_object(
Box::new(GamepadButton::new_inherited(pressed, touched)),
window,
can_gc,
)
}
}
impl GamepadButtonMethods<crate::DomTypeHolder> for GamepadButton {
// https://www.w3.org/TR/gamepad/#widl-GamepadButton-pressed
fn Pressed(&self) -> bool {
self.pressed.get()
}
// https://www.w3.org/TR/gamepad/#widl-GamepadButton-touched
fn Touched(&self) -> bool {
self.touched.get()
}
// https://www.w3.org/TR/gamepad/#widl-GamepadButton-value
fn Value(&self) -> Finite<f64> {
Finite::wrap(self.value.get())
}
}
impl GamepadButton {
pub(crate) fn update(&self, pressed: bool, touched: bool, value: f64) {
self.pressed.set(pressed);
self.touched.set(touched);
self.value.set(value);
}
}

View file

@ -0,0 +1,88 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use dom_struct::dom_struct;
use crate::dom::bindings::codegen::Bindings::GamepadButtonListBinding::GamepadButtonListMethods;
use crate::dom::bindings::reflector::{Reflector, reflect_dom_object};
use crate::dom::bindings::root::{Dom, DomRoot, DomSlice};
use crate::dom::gamepad::gamepadbutton::GamepadButton;
use crate::dom::window::Window;
use crate::script_runtime::CanGc;
// https://w3c.github.io/gamepad/#gamepadbutton-interface
#[dom_struct]
pub(crate) struct GamepadButtonList {
reflector_: Reflector,
list: Vec<Dom<GamepadButton>>,
}
impl GamepadButtonList {
#[cfg_attr(crown, allow(crown::unrooted_must_root))]
fn new_inherited(list: &[&GamepadButton]) -> GamepadButtonList {
GamepadButtonList {
reflector_: Reflector::new(),
list: list.iter().map(|button| Dom::from_ref(*button)).collect(),
}
}
pub(crate) fn new(
window: &Window,
list: &[&GamepadButton],
can_gc: CanGc,
) -> DomRoot<GamepadButtonList> {
reflect_dom_object(
Box::new(GamepadButtonList::new_inherited(list)),
window,
can_gc,
)
}
}
impl GamepadButtonListMethods<crate::DomTypeHolder> for GamepadButtonList {
// https://w3c.github.io/gamepad/#dom-gamepad-buttons
fn Length(&self) -> u32 {
self.list.len() as u32
}
// https://w3c.github.io/gamepad/#dom-gamepad-buttons
fn Item(&self, index: u32) -> Option<DomRoot<GamepadButton>> {
self.list
.get(index as usize)
.map(|button| DomRoot::from_ref(&**button))
}
// https://w3c.github.io/gamepad/#dom-gamepad-buttons
fn IndexedGetter(&self, index: u32) -> Option<DomRoot<GamepadButton>> {
self.Item(index)
}
}
impl GamepadButtonList {
/// Initialize the number of buttons in the "standard" gamepad mapping.
/// <https://www.w3.org/TR/gamepad/#dfn-initializing-buttons>
pub(crate) fn init_buttons(window: &Window, can_gc: CanGc) -> DomRoot<GamepadButtonList> {
let standard_buttons = &[
GamepadButton::new(window, false, false, can_gc), // Bottom button in right cluster
GamepadButton::new(window, false, false, can_gc), // Right button in right cluster
GamepadButton::new(window, false, false, can_gc), // Left button in right cluster
GamepadButton::new(window, false, false, can_gc), // Top button in right cluster
GamepadButton::new(window, false, false, can_gc), // Top left front button
GamepadButton::new(window, false, false, can_gc), // Top right front button
GamepadButton::new(window, false, false, can_gc), // Bottom left front button
GamepadButton::new(window, false, false, can_gc), // Bottom right front button
GamepadButton::new(window, false, false, can_gc), // Left button in center cluster
GamepadButton::new(window, false, false, can_gc), // Right button in center cluster
GamepadButton::new(window, false, false, can_gc), // Left stick pressed button
GamepadButton::new(window, false, false, can_gc), // Right stick pressed button
GamepadButton::new(window, false, false, can_gc), // Top button in left cluster
GamepadButton::new(window, false, false, can_gc), // Bottom button in left cluster
GamepadButton::new(window, false, false, can_gc), // Left button in left cluster
GamepadButton::new(window, false, false, can_gc), // Right button in left cluster
GamepadButton::new(window, false, false, can_gc), // Center button in center cluster
];
rooted_vec!(let buttons <- standard_buttons.iter().map(DomRoot::as_traced));
Self::new(window, buttons.r(), can_gc)
}
}

View file

@ -0,0 +1,118 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use dom_struct::dom_struct;
use js::rust::HandleObject;
use stylo_atoms::Atom;
use super::gamepad::Gamepad;
use crate::dom::bindings::codegen::Bindings::EventBinding::Event_Binding::EventMethods;
use crate::dom::bindings::codegen::Bindings::GamepadEventBinding;
use crate::dom::bindings::codegen::Bindings::GamepadEventBinding::GamepadEventMethods;
use crate::dom::bindings::error::Fallible;
use crate::dom::bindings::inheritance::Castable;
use crate::dom::bindings::reflector::reflect_dom_object_with_proto;
use crate::dom::bindings::root::{Dom, DomRoot};
use crate::dom::bindings::str::DOMString;
use crate::dom::event::Event;
use crate::dom::window::Window;
use crate::script_runtime::CanGc;
#[dom_struct]
pub(crate) struct GamepadEvent {
event: Event,
gamepad: Dom<Gamepad>,
}
pub(crate) enum GamepadEventType {
Connected,
Disconnected,
}
impl GamepadEvent {
fn new_inherited(gamepad: &Gamepad) -> GamepadEvent {
GamepadEvent {
event: Event::new_inherited(),
gamepad: Dom::from_ref(gamepad),
}
}
pub(crate) fn new(
window: &Window,
type_: Atom,
bubbles: bool,
cancelable: bool,
gamepad: &Gamepad,
can_gc: CanGc,
) -> DomRoot<GamepadEvent> {
Self::new_with_proto(window, None, type_, bubbles, cancelable, gamepad, can_gc)
}
fn new_with_proto(
window: &Window,
proto: Option<HandleObject>,
type_: Atom,
bubbles: bool,
cancelable: bool,
gamepad: &Gamepad,
can_gc: CanGc,
) -> DomRoot<GamepadEvent> {
let ev = reflect_dom_object_with_proto(
Box::new(GamepadEvent::new_inherited(gamepad)),
window,
proto,
can_gc,
);
{
let event = ev.upcast::<Event>();
event.init_event(type_, bubbles, cancelable);
}
ev
}
pub(crate) fn new_with_type(
window: &Window,
event_type: GamepadEventType,
gamepad: &Gamepad,
can_gc: CanGc,
) -> DomRoot<GamepadEvent> {
let name = match event_type {
GamepadEventType::Connected => "gamepadconnected",
GamepadEventType::Disconnected => "gamepaddisconnected",
};
GamepadEvent::new(window, name.into(), false, false, gamepad, can_gc)
}
}
impl GamepadEventMethods<crate::DomTypeHolder> for GamepadEvent {
// https://w3c.github.io/gamepad/#gamepadevent-interface
fn Constructor(
window: &Window,
proto: Option<HandleObject>,
can_gc: CanGc,
type_: DOMString,
init: &GamepadEventBinding::GamepadEventInit,
) -> Fallible<DomRoot<GamepadEvent>> {
Ok(GamepadEvent::new_with_proto(
window,
proto,
Atom::from(type_),
init.parent.bubbles,
init.parent.cancelable,
&init.gamepad,
can_gc,
))
}
// https://w3c.github.io/gamepad/#gamepadevent-interface
fn Gamepad(&self) -> DomRoot<Gamepad> {
DomRoot::from_ref(&*self.gamepad)
}
// https://dom.spec.whatwg.org/#dom-event-istrusted
fn IsTrusted(&self) -> bool {
self.event.IsTrusted()
}
}

View file

@ -0,0 +1,383 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::cell::Cell;
use std::rc::Rc;
use dom_struct::dom_struct;
use embedder_traits::{DualRumbleEffectParams, EmbedderMsg, GamepadSupportedHapticEffects};
use ipc_channel::ipc;
use ipc_channel::router::ROUTER;
use js::rust::MutableHandleValue;
use crate::dom::bindings::cell::DomRefCell;
use crate::dom::bindings::codegen::Bindings::GamepadHapticActuatorBinding::{
GamepadEffectParameters, GamepadHapticActuatorMethods, GamepadHapticEffectType,
};
use crate::dom::bindings::codegen::Bindings::WindowBinding::Window_Binding::WindowMethods;
use crate::dom::bindings::error::Error;
use crate::dom::bindings::refcounted::{Trusted, TrustedPromise};
use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object};
use crate::dom::bindings::root::DomRoot;
use crate::dom::bindings::str::DOMString;
use crate::dom::bindings::utils::to_frozen_array;
use crate::dom::promise::Promise;
use crate::dom::window::Window;
use crate::realms::InRealm;
use crate::script_runtime::{CanGc, JSContext};
use crate::task_source::SendableTaskSource;
struct HapticEffectListener {
task_source: SendableTaskSource,
context: Trusted<GamepadHapticActuator>,
}
impl HapticEffectListener {
fn handle_stopped(&self, stopped_successfully: bool) {
let context = self.context.clone();
self.task_source
.queue(task!(handle_haptic_effect_stopped: move || {
let actuator = context.root();
actuator.handle_haptic_effect_stopped(stopped_successfully);
}));
}
fn handle_completed(&self, completed_successfully: bool) {
let context = self.context.clone();
self.task_source
.queue(task!(handle_haptic_effect_completed: move || {
let actuator = context.root();
actuator.handle_haptic_effect_completed(completed_successfully, CanGc::note());
}));
}
}
/// <https://www.w3.org/TR/gamepad/#gamepadhapticactuator-interface>
#[dom_struct]
pub(crate) struct GamepadHapticActuator {
reflector_: Reflector,
gamepad_index: u32,
/// <https://www.w3.org/TR/gamepad/#dfn-effects>
effects: Vec<GamepadHapticEffectType>,
/// <https://www.w3.org/TR/gamepad/#dfn-playingeffectpromise>
#[ignore_malloc_size_of = "Rc is hard"]
playing_effect_promise: DomRefCell<Option<Rc<Promise>>>,
/// The current sequence ID for playing effects,
/// incremented on every call to playEffect() or reset().
/// Used to ensure that promises are resolved correctly.
/// Based on this pending PR <https://github.com/w3c/gamepad/pull/201>
sequence_id: Cell<u32>,
/// The sequence ID during the last playEffect() call
effect_sequence_id: Cell<u32>,
/// The sequence ID during the last reset() call
reset_sequence_id: Cell<u32>,
}
impl GamepadHapticActuator {
fn new_inherited(
gamepad_index: u32,
supported_haptic_effects: GamepadSupportedHapticEffects,
) -> GamepadHapticActuator {
let mut effects = vec![];
if supported_haptic_effects.supports_dual_rumble {
effects.push(GamepadHapticEffectType::Dual_rumble);
}
if supported_haptic_effects.supports_trigger_rumble {
effects.push(GamepadHapticEffectType::Trigger_rumble);
}
Self {
reflector_: Reflector::new(),
gamepad_index,
effects,
playing_effect_promise: DomRefCell::new(None),
sequence_id: Cell::new(0),
effect_sequence_id: Cell::new(0),
reset_sequence_id: Cell::new(0),
}
}
pub(crate) fn new(
window: &Window,
gamepad_index: u32,
supported_haptic_effects: GamepadSupportedHapticEffects,
can_gc: CanGc,
) -> DomRoot<GamepadHapticActuator> {
reflect_dom_object(
Box::new(GamepadHapticActuator::new_inherited(
gamepad_index,
supported_haptic_effects,
)),
window,
can_gc,
)
}
}
impl GamepadHapticActuatorMethods<crate::DomTypeHolder> for GamepadHapticActuator {
/// <https://www.w3.org/TR/gamepad/#dom-gamepadhapticactuator-effects>
fn Effects(&self, cx: JSContext, can_gc: CanGc, retval: MutableHandleValue) {
to_frozen_array(self.effects.as_slice(), cx, retval, can_gc)
}
/// <https://www.w3.org/TR/gamepad/#dom-gamepadhapticactuator-playeffect>
fn PlayEffect(
&self,
type_: GamepadHapticEffectType,
params: &GamepadEffectParameters,
comp: InRealm,
can_gc: CanGc,
) -> Rc<Promise> {
let playing_effect_promise = Promise::new_in_current_realm(comp, can_gc);
// <https://www.w3.org/TR/gamepad/#dfn-valid-effect>
match type_ {
// <https://www.w3.org/TR/gamepad/#dfn-valid-dual-rumble-effect>
GamepadHapticEffectType::Dual_rumble => {
if *params.strongMagnitude < 0.0 || *params.strongMagnitude > 1.0 {
playing_effect_promise.reject_error(
Error::Type(
"Strong magnitude value is not within range of 0.0 to 1.0.".to_string(),
),
can_gc,
);
return playing_effect_promise;
} else if *params.weakMagnitude < 0.0 || *params.weakMagnitude > 1.0 {
playing_effect_promise.reject_error(
Error::Type(
"Weak magnitude value is not within range of 0.0 to 1.0.".to_string(),
),
can_gc,
);
return playing_effect_promise;
}
},
// <https://www.w3.org/TR/gamepad/#dfn-valid-trigger-rumble-effect>
GamepadHapticEffectType::Trigger_rumble => {
if *params.strongMagnitude < 0.0 || *params.strongMagnitude > 1.0 {
playing_effect_promise.reject_error(
Error::Type(
"Strong magnitude value is not within range of 0.0 to 1.0.".to_string(),
),
can_gc,
);
return playing_effect_promise;
} else if *params.weakMagnitude < 0.0 || *params.weakMagnitude > 1.0 {
playing_effect_promise.reject_error(
Error::Type(
"Weak magnitude value is not within range of 0.0 to 1.0.".to_string(),
),
can_gc,
);
return playing_effect_promise;
} else if *params.leftTrigger < 0.0 || *params.leftTrigger > 1.0 {
playing_effect_promise.reject_error(
Error::Type(
"Left trigger value is not within range of 0.0 to 1.0.".to_string(),
),
can_gc,
);
return playing_effect_promise;
} else if *params.rightTrigger < 0.0 || *params.rightTrigger > 1.0 {
playing_effect_promise.reject_error(
Error::Type(
"Right trigger value is not within range of 0.0 to 1.0.".to_string(),
),
can_gc,
);
return playing_effect_promise;
}
},
}
let document = self.global().as_window().Document();
if !document.is_fully_active() {
playing_effect_promise.reject_error(Error::InvalidState, can_gc);
}
self.sequence_id.set(self.sequence_id.get().wrapping_add(1));
if let Some(promise) = self.playing_effect_promise.borrow_mut().take() {
let trusted_promise = TrustedPromise::new(promise);
self.global().task_manager().gamepad_task_source().queue(
task!(preempt_promise: move || {
let promise = trusted_promise.root();
let message = DOMString::from("preempted");
promise.resolve_native(&message, CanGc::note());
}),
);
}
if !self.effects.contains(&type_) {
playing_effect_promise.reject_error(Error::NotSupported, can_gc);
return playing_effect_promise;
}
*self.playing_effect_promise.borrow_mut() = Some(playing_effect_promise.clone());
self.effect_sequence_id.set(self.sequence_id.get());
let context = Trusted::new(self);
let (effect_complete_sender, effect_complete_receiver) =
ipc::channel().expect("ipc channel failure");
let listener = HapticEffectListener {
task_source: self.global().task_manager().gamepad_task_source().into(),
context,
};
ROUTER.add_typed_route(
effect_complete_receiver,
Box::new(move |message| match message {
Ok(msg) => listener.handle_completed(msg),
Err(err) => warn!("Error receiving a GamepadMsg: {:?}", err),
}),
);
// Note: The spec says we SHOULD also pass a playEffectTimestamp for more precise playback timing
// when start_delay is non-zero, but this is left more as a footnote without much elaboration.
// <https://www.w3.org/TR/gamepad/#dfn-issue-a-haptic-effect>
let params = DualRumbleEffectParams {
duration: params.duration as f64,
start_delay: params.startDelay as f64,
strong_magnitude: *params.strongMagnitude,
weak_magnitude: *params.weakMagnitude,
};
let event = EmbedderMsg::PlayGamepadHapticEffect(
document.webview_id(),
self.gamepad_index as usize,
embedder_traits::GamepadHapticEffectType::DualRumble(params),
effect_complete_sender,
);
self.global().as_window().send_to_embedder(event);
playing_effect_promise
}
/// <https://www.w3.org/TR/gamepad/#dom-gamepadhapticactuator-reset>
fn Reset(&self, comp: InRealm, can_gc: CanGc) -> Rc<Promise> {
let promise = Promise::new_in_current_realm(comp, can_gc);
let document = self.global().as_window().Document();
if !document.is_fully_active() {
promise.reject_error(Error::InvalidState, can_gc);
return promise;
}
self.sequence_id.set(self.sequence_id.get().wrapping_add(1));
if let Some(promise) = self.playing_effect_promise.borrow_mut().take() {
let trusted_promise = TrustedPromise::new(promise);
self.global().task_manager().gamepad_task_source().queue(
task!(preempt_promise: move || {
let promise = trusted_promise.root();
let message = DOMString::from("preempted");
promise.resolve_native(&message, CanGc::note());
}),
);
}
*self.playing_effect_promise.borrow_mut() = Some(promise.clone());
self.reset_sequence_id.set(self.sequence_id.get());
let context = Trusted::new(self);
let (effect_stop_sender, effect_stop_receiver) =
ipc::channel().expect("ipc channel failure");
let listener = HapticEffectListener {
task_source: self.global().task_manager().gamepad_task_source().into(),
context,
};
ROUTER.add_typed_route(
effect_stop_receiver,
Box::new(move |message| match message {
Ok(msg) => listener.handle_stopped(msg),
Err(err) => warn!("Error receiving a GamepadMsg: {:?}", err),
}),
);
let event = EmbedderMsg::StopGamepadHapticEffect(
document.webview_id(),
self.gamepad_index as usize,
effect_stop_sender,
);
self.global().as_window().send_to_embedder(event);
self.playing_effect_promise.borrow().clone().unwrap()
}
}
impl GamepadHapticActuator {
/// <https://www.w3.org/TR/gamepad/#dom-gamepadhapticactuator-playeffect>
/// We are in the task queued by the "in-parallel" steps.
pub(crate) fn handle_haptic_effect_completed(
&self,
completed_successfully: bool,
can_gc: CanGc,
) {
if self.effect_sequence_id.get() != self.sequence_id.get() || !completed_successfully {
return;
}
let playing_effect_promise = self.playing_effect_promise.borrow_mut().take();
if let Some(promise) = playing_effect_promise {
let message = DOMString::from("complete");
promise.resolve_native(&message, can_gc);
}
}
/// <https://www.w3.org/TR/gamepad/#dom-gamepadhapticactuator-reset>
/// We are in the task queued by the "in-parallel" steps.
pub(crate) fn handle_haptic_effect_stopped(&self, stopped_successfully: bool) {
if !stopped_successfully {
return;
}
let playing_effect_promise = self.playing_effect_promise.borrow_mut().take();
if let Some(promise) = playing_effect_promise {
let trusted_promise = TrustedPromise::new(promise);
let sequence_id = self.sequence_id.get();
let reset_sequence_id = self.reset_sequence_id.get();
self.global().task_manager().gamepad_task_source().queue(
task!(complete_promise: move || {
if sequence_id != reset_sequence_id {
warn!("Mismatched sequence/reset sequence ids: {} != {}", sequence_id, reset_sequence_id);
return;
}
let promise = trusted_promise.root();
let message = DOMString::from("complete");
promise.resolve_native(&message, CanGc::note());
})
);
}
}
/// <https://www.w3.org/TR/gamepad/#handling-visibility-change>
pub(crate) fn handle_visibility_change(&self) {
if self.playing_effect_promise.borrow().is_none() {
return;
}
let this = Trusted::new(self);
self.global().task_manager().gamepad_task_source().queue(
task!(stop_playing_effect: move || {
let actuator = this.root();
let Some(promise) = actuator.playing_effect_promise.borrow_mut().take() else {
return;
};
let message = DOMString::from("preempted");
promise.resolve_native(&message, CanGc::note());
}),
);
let (send, _rcv) = ipc::channel().expect("ipc channel failure");
let document = self.global().as_window().Document();
let event = EmbedderMsg::StopGamepadHapticEffect(
document.webview_id(),
self.gamepad_index as usize,
send,
);
self.global().as_window().send_to_embedder(event);
}
}

View file

@ -0,0 +1,92 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use dom_struct::dom_struct;
use js::typedarray::{Float32, Float32Array};
use crate::dom::bindings::buffer_source::HeapBufferSource;
use crate::dom::bindings::codegen::Bindings::GamepadPoseBinding::GamepadPoseMethods;
use crate::dom::bindings::reflector::{Reflector, reflect_dom_object};
use crate::dom::bindings::root::DomRoot;
use crate::dom::globalscope::GlobalScope;
use crate::script_runtime::{CanGc, JSContext};
#[dom_struct]
pub(crate) struct GamepadPose {
reflector_: Reflector,
#[ignore_malloc_size_of = "mozjs"]
position: HeapBufferSource<Float32>,
#[ignore_malloc_size_of = "mozjs"]
orientation: HeapBufferSource<Float32>,
#[ignore_malloc_size_of = "mozjs"]
linear_vel: HeapBufferSource<Float32>,
#[ignore_malloc_size_of = "mozjs"]
angular_vel: HeapBufferSource<Float32>,
#[ignore_malloc_size_of = "mozjs"]
linear_acc: HeapBufferSource<Float32>,
#[ignore_malloc_size_of = "mozjs"]
angular_acc: HeapBufferSource<Float32>,
}
// TODO: support gamepad discovery
#[allow(dead_code)]
impl GamepadPose {
fn new_inherited() -> GamepadPose {
GamepadPose {
reflector_: Reflector::new(),
position: HeapBufferSource::default(),
orientation: HeapBufferSource::default(),
linear_vel: HeapBufferSource::default(),
angular_vel: HeapBufferSource::default(),
linear_acc: HeapBufferSource::default(),
angular_acc: HeapBufferSource::default(),
}
}
pub(crate) fn new(global: &GlobalScope, can_gc: CanGc) -> DomRoot<GamepadPose> {
reflect_dom_object(Box::new(GamepadPose::new_inherited()), global, can_gc)
}
}
impl GamepadPoseMethods<crate::DomTypeHolder> for GamepadPose {
// https://w3c.github.io/gamepad/extensions.html#dom-gamepadpose-position
fn GetPosition(&self, _cx: JSContext) -> Option<Float32Array> {
self.position.typed_array_to_option()
}
// https://w3c.github.io/gamepad/extensions.html#dom-gamepadpose-hasposition
fn HasPosition(&self) -> bool {
self.position.is_initialized()
}
// https://w3c.github.io/gamepad/extensions.html#dom-gamepadpose-linearvelocity
fn GetLinearVelocity(&self, _cx: JSContext) -> Option<Float32Array> {
self.linear_vel.typed_array_to_option()
}
// https://w3c.github.io/gamepad/extensions.html#dom-gamepadpose-linearacceleration
fn GetLinearAcceleration(&self, _cx: JSContext) -> Option<Float32Array> {
self.linear_acc.typed_array_to_option()
}
// https://w3c.github.io/gamepad/extensions.html#dom-gamepadpose-orientation
fn GetOrientation(&self, _cx: JSContext) -> Option<Float32Array> {
self.orientation.typed_array_to_option()
}
// https://w3c.github.io/gamepad/extensions.html#dom-gamepadpose-orientation
fn HasOrientation(&self) -> bool {
self.orientation.is_initialized()
}
// https://w3c.github.io/gamepad/extensions.html#dom-gamepadpose-angularvelocity
fn GetAngularVelocity(&self, _cx: JSContext) -> Option<Float32Array> {
self.angular_vel.typed_array_to_option()
}
// https://w3c.github.io/gamepad/extensions.html#dom-gamepadpose-angularacceleration
fn GetAngularAcceleration(&self, _cx: JSContext) -> Option<Float32Array> {
self.angular_acc.typed_array_to_option()
}
}

View file

@ -0,0 +1,12 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
#[expect(clippy::module_inception, reason = "The interface name is Gamepad")]
pub(crate) mod gamepad;
pub(crate) use gamepad::Gamepad;
pub(crate) mod gamepadbutton;
pub(crate) mod gamepadbuttonlist;
pub(crate) mod gamepadevent;
pub(crate) mod gamepadhapticactuator;
pub(crate) mod gamepadpose;