Implement non-XR Gamepad discovery and input (#31200)

* Create embedder event to send to constellation

* Handle gamepad message in constellation, send to script thread

* Handle GamepadEvent in script thread and dispatch event to document

* Add missing Clones, fix event

* Add gamepad task source

* Adjust GamepadIndex type, remove unused imports

* Add internal getter for gamepads list

* Update gamepad new methods

* Handle gamepad connect and disconnect events

* Proto will be none, no need for HandleObject

* Initialize buttons and axes to standard mapping

* Adjust update type index types

* Update GamepadButton update function

* Adjust Gamepad mapping comments to match spec, add update logic

* Amend comment

* Update button and axis inputs on Updated event

* Add GilRs as gamepad backend in servoshell

* Add spec links, queue gamepad updates on task source

* ./mach fmt

* Fix comment length

* Split out button init, update spec comments

* Move gamepad event handling from document to global

* Map and normalize axes/button values

* Use std::time for gamepad timestamp

* Adjust gamepad handling in event loop

* Move button press/touch check into map+normalize function

- Small change but is more in line with spec

* ./mach fmt

* Update comment spec links and warning messages

* Doc comments -> regular comments

* Add window event handlers for gamepad connect/disconnect

* Adjust gamepad disconnect behavior

* Add missing TODO's, adjust gamepad/gamepadbutton list methods and formatting

* Update button handling from gilrs, add comments

* Enable gamepad pref during WPT tests and update expectations

* Update WPT expectations in meta-legacy-layout
This commit is contained in:
Daniel Adams 2024-02-17 08:42:31 -10:00 committed by GitHub
parent 1cc546c4fc
commit c999d4546c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 695 additions and 208 deletions

86
Cargo.lock generated
View file

@ -2005,6 +2005,40 @@ dependencies = [
"weezl",
]
[[package]]
name = "gilrs"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b2e57a9cb946b5d04ae8638c5f554abb5a9f82c4c950fd5b1fee6d119592fb"
dependencies = [
"fnv",
"gilrs-core",
"log",
"uuid",
"vec_map",
]
[[package]]
name = "gilrs-core"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0af1827b7dd2f36d740ae804c1b3ea0d64c12533fb61ff91883005143a0e8c5a"
dependencies = [
"core-foundation",
"inotify",
"io-kit-sys",
"js-sys",
"libc",
"libudev-sys",
"log",
"nix 0.27.1",
"uuid",
"vec_map",
"wasm-bindgen",
"web-sys",
"windows",
]
[[package]]
name = "gimli"
version = "0.28.1"
@ -2807,6 +2841,26 @@ dependencies = [
"serde",
]
[[package]]
name = "inotify"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc"
dependencies = [
"bitflags 1.3.2",
"inotify-sys",
"libc",
]
[[package]]
name = "inotify-sys"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
dependencies = [
"libc",
]
[[package]]
name = "instant"
version = "0.1.12"
@ -2819,6 +2873,16 @@ dependencies = [
"web-sys",
]
[[package]]
name = "io-kit-sys"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4769cb30e5dcf1710fc6730d3e94f78c47723a014a567de385e113c737394640"
dependencies = [
"core-foundation-sys",
"mach2",
]
[[package]]
name = "io-surface"
version = "0.15.1"
@ -3329,6 +3393,16 @@ dependencies = [
"webxr-api",
]
[[package]]
name = "libudev-sys"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324"
dependencies = [
"libc",
"pkg-config",
]
[[package]]
name = "libz-sys"
version = "1.1.15"
@ -3879,6 +3953,17 @@ dependencies = [
"pin-utils",
]
[[package]]
name = "nix"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
dependencies = [
"bitflags 2.4.2",
"cfg-if",
"libc",
]
[[package]]
name = "nodrop"
version = "0.1.14"
@ -5396,6 +5481,7 @@ dependencies = [
"egui_glow",
"euclid",
"getopts",
"gilrs",
"gleam",
"glow",
"image",

View file

@ -13,7 +13,9 @@ use gfx::rendering_context::RenderingContext;
use keyboard_types::KeyboardEvent;
use libc::c_void;
use msg::constellation_msg::{PipelineId, TopLevelBrowsingContextId, TraversalDirection};
use script_traits::{MediaSessionActionType, MouseButton, TouchEventType, TouchId, WheelDelta};
use script_traits::{
GamepadEvent, MediaSessionActionType, MouseButton, TouchEventType, TouchId, WheelDelta,
};
use servo_geometry::DeviceIndependentPixel;
use servo_url::ServoUrl;
use style_traits::DevicePixel;
@ -113,6 +115,8 @@ pub enum EmbedderEvent {
/// the native widget when it is brough back to foreground. This event
/// carries the pointer to the native widget and its new size.
ReplaceNativeSurface(*mut c_void, DeviceIntSize),
/// Sent when new Gamepad information is available.
Gamepad(GamepadEvent),
}
impl Debug for EmbedderEvent {
@ -149,6 +153,7 @@ impl Debug for EmbedderEvent {
EmbedderEvent::ClearCache => write!(f, "ClearCache"),
EmbedderEvent::InvalidateNativeSurface => write!(f, "InvalidateNativeSurface"),
EmbedderEvent::ReplaceNativeSurface(..) => write!(f, "ReplaceNativeSurface"),
EmbedderEvent::Gamepad(..) => write!(f, "Gamepad"),
}
}
}

View file

@ -142,7 +142,7 @@ use script_traits::CompositorEvent::{MouseButtonEvent, MouseMoveEvent};
use script_traits::{
webdriver_msg, AnimationState, AnimationTickType, AuxiliaryBrowsingContextLoadInfo,
BroadcastMsg, CompositorEvent, ConstellationControlMsg, DiscardBrowsingContext,
DocumentActivity, DocumentState, HistoryEntryReplacement, IFrameLoadInfo,
DocumentActivity, DocumentState, GamepadEvent, HistoryEntryReplacement, IFrameLoadInfo,
IFrameLoadInfoWithData, IFrameSandboxState, IFrameSizeMsg, Job, LayoutControlMsg,
LayoutMsg as FromLayoutMsg, LoadData, LoadOrigin, LogEntry, MediaSessionActionType,
MessagePortMsg, MouseEventType, PortMessageTask, SWManagerMsg, SWManagerSenders,
@ -1550,6 +1550,9 @@ where
EmbedderMsg::ReadyToPresent,
));
},
FromCompositorMsg::Gamepad(gamepad_event) => {
self.handle_gamepad_msg(gamepad_event);
},
}
}
@ -5466,4 +5469,40 @@ where
error!("Got a media session action but no active media session is registered");
}
}
/// Handle GamepadEvents from the embedder and forward them to the script thread
fn handle_gamepad_msg(&mut self, event: GamepadEvent) {
// Send to the focused browsing contexts' current pipeline.
let focused_browsing_context_id = self
.webviews
.focused_webview()
.map(|(_, webview)| webview.focused_browsing_context_id);
match focused_browsing_context_id {
Some(browsing_context_id) => {
let event = CompositorEvent::GamepadEvent(event);
let pipeline_id = match self.browsing_contexts.get(&browsing_context_id) {
Some(ctx) => ctx.pipeline_id,
None => {
return warn!(
"{}: Got gamepad event for nonexistent browsing context",
browsing_context_id,
);
},
};
let msg = ConstellationControlMsg::SendEvent(pipeline_id, event);
let result = match self.pipelines.get(&pipeline_id) {
Some(pipeline) => pipeline.event_loop.send(msg),
None => {
return debug!("{}: Got gamepad event after closure", pipeline_id);
},
};
if let Err(e) = result {
self.handle_send_error(pipeline_id, e);
}
},
None => {
warn!("No focused webview to handle gamepad event");
},
}
}
}

View file

@ -9,9 +9,10 @@ use js::typedarray::{Float64, Float64Array};
use super::bindings::typedarrays::HeapTypedArray;
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::{DomObject, Reflector};
use crate::dom::bindings::reflector::{reflect_dom_object_with_proto, DomObject, Reflector};
use crate::dom::bindings::root::{Dom, DomRoot};
use crate::dom::bindings::str::DOMString;
use crate::dom::event::Event;
@ -19,8 +20,13 @@ use crate::dom::eventtarget::EventTarget;
use crate::dom::gamepadbuttonlist::GamepadButtonList;
use crate::dom::gamepadevent::{GamepadEvent, GamepadEventType};
use crate::dom::gamepadpose::GamepadPose;
use crate::dom::globalscope::GlobalScope;
use crate::script_runtime::JSContext;
// 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 struct Gamepad {
reflector_: Reflector,
@ -36,10 +42,10 @@ pub struct Gamepad {
pose: Option<Dom<GamepadPose>>,
#[ignore_malloc_size_of = "Defined in rust-webvr"]
hand: GamepadHand,
axis_bounds: (f64, f64),
button_bounds: (f64, f64),
}
// TODO: support gamepad discovery
#[allow(dead_code)]
impl Gamepad {
fn new_inherited(
gamepad_id: u32,
@ -51,6 +57,8 @@ impl Gamepad {
buttons: &GamepadButtonList,
pose: Option<&GamepadPose>,
hand: GamepadHand,
axis_bounds: (f64, f64),
button_bounds: (f64, f64),
) -> Gamepad {
Self {
reflector_: Reflector::new(),
@ -64,8 +72,54 @@ impl Gamepad {
buttons: Dom::from_ref(buttons),
pose: pose.map(Dom::from_ref),
hand: hand,
axis_bounds: axis_bounds,
button_bounds: button_bounds,
}
}
pub fn new(
global: &GlobalScope,
gamepad_id: u32,
id: String,
axis_bounds: (f64, f64),
button_bounds: (f64, f64),
) -> DomRoot<Gamepad> {
Self::new_with_proto(global, gamepad_id, id, axis_bounds, button_bounds)
}
/// 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>
fn new_with_proto(
global: &GlobalScope,
gamepad_id: u32,
id: String,
axis_bounds: (f64, f64),
button_bounds: (f64, f64),
) -> DomRoot<Gamepad> {
let button_list = GamepadButtonList::init_buttons(global);
let gamepad = reflect_dom_object_with_proto(
Box::new(Gamepad::new_inherited(
gamepad_id,
id,
0,
false,
0.,
String::from("standard"),
&button_list,
None,
GamepadHand::_empty,
axis_bounds,
button_bounds,
)),
global,
None,
);
gamepad.init_axes();
gamepad
}
}
impl GamepadMethods for Gamepad {
@ -117,7 +171,6 @@ impl GamepadMethods for Gamepad {
}
}
// TODO: support gamepad discovery
#[allow(dead_code)]
impl Gamepad {
pub fn gamepad_id(&self) -> u32 {
@ -143,10 +196,73 @@ impl Gamepad {
self.index.set(index);
}
pub fn update_timestamp(&self, timestamp: f64) {
self.timestamp.set(timestamp);
}
pub fn notify_event(&self, event_type: GamepadEventType) {
let event = GamepadEvent::new_with_type(&self.global(), event_type, &self);
event
.upcast::<Event>()
.fire(self.global().as_window().upcast::<EventTarget>());
}
/// Initialize the number of axes in the "standard" gamepad mapping.
/// <https://www.w3.org/TR/gamepad/#dfn-initializing-axes>
fn init_axes(&self) {
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)
.expect("Failed to set axes data on gamepad.")
}
#[allow(unsafe_code)]
/// <https://www.w3.org/TR/gamepad/#dfn-map-and-normalize-axes>
pub 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
.internal_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 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!");
}
}
}

View file

@ -20,8 +20,6 @@ pub struct GamepadButton {
value: Cell<f64>,
}
// TODO: support gamepad discovery
#[allow(dead_code)]
impl GamepadButton {
pub fn new_inherited(pressed: bool, touched: bool) -> GamepadButton {
Self {
@ -57,11 +55,10 @@ impl GamepadButtonMethods for GamepadButton {
}
}
// TODO: support gamepad discovery
#[allow(dead_code)]
impl GamepadButton {
pub fn update(&self, pressed: bool, touched: bool) {
pub fn update(&self, pressed: bool, touched: bool, value: f64) {
self.pressed.set(pressed);
self.touched.set(touched);
self.value.set(value);
}
}

View file

@ -5,9 +5,10 @@
use dom_struct::dom_struct;
use crate::dom::bindings::codegen::Bindings::GamepadButtonListBinding::GamepadButtonListMethods;
use crate::dom::bindings::reflector::Reflector;
use crate::dom::bindings::root::{Dom, DomRoot};
use crate::dom::bindings::reflector::{reflect_dom_object, Reflector};
use crate::dom::bindings::root::{Dom, DomRoot, DomSlice};
use crate::dom::gamepadbutton::GamepadButton;
use crate::dom::globalscope::GlobalScope;
// https://w3c.github.io/gamepad/#gamepadbutton-interface
#[dom_struct]
@ -16,8 +17,6 @@ pub struct GamepadButtonList {
list: Vec<Dom<GamepadButton>>,
}
// TODO: support gamepad discovery
#[allow(dead_code)]
impl GamepadButtonList {
#[allow(crown::unrooted_must_root)]
fn new_inherited(list: &[&GamepadButton]) -> GamepadButtonList {
@ -26,6 +25,10 @@ impl GamepadButtonList {
list: list.iter().map(|button| Dom::from_ref(*button)).collect(),
}
}
pub fn new(global: &GlobalScope, list: &[&GamepadButton]) -> DomRoot<GamepadButtonList> {
reflect_dom_object(Box::new(GamepadButtonList::new_inherited(list)), global)
}
}
impl GamepadButtonListMethods for GamepadButtonList {
@ -46,3 +49,31 @@ impl GamepadButtonListMethods for GamepadButtonList {
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 fn init_buttons(global: &GlobalScope) -> DomRoot<GamepadButtonList> {
let standard_buttons = &[
GamepadButton::new(global, false, false), // Bottom button in right cluster
GamepadButton::new(global, false, false), // Right button in right cluster
GamepadButton::new(global, false, false), // Left button in right cluster
GamepadButton::new(global, false, false), // Top button in right cluster
GamepadButton::new(global, false, false), // Top left front button
GamepadButton::new(global, false, false), // Top right front button
GamepadButton::new(global, false, false), // Bottom left front button
GamepadButton::new(global, false, false), // Bottom right front button
GamepadButton::new(global, false, false), // Left button in center cluster
GamepadButton::new(global, false, false), // Right button in center cluster
GamepadButton::new(global, false, false), // Left stick pressed button
GamepadButton::new(global, false, false), // Right stick pressed button
GamepadButton::new(global, false, false), // Top button in left cluster
GamepadButton::new(global, false, false), // Bottom button in left cluster
GamepadButton::new(global, false, false), // Left button in left cluster
GamepadButton::new(global, false, false), // Right button in left cluster
GamepadButton::new(global, false, false), // Center button in center cluster
];
rooted_vec!(let buttons <- standard_buttons.iter().map(|button| DomRoot::from_ref(&**button)));
Self::new(global, buttons.r())
}
}

View file

@ -18,8 +18,6 @@ pub struct GamepadList {
list: DomRefCell<Vec<Dom<Gamepad>>>,
}
// TODO: support gamepad discovery
#[allow(dead_code)]
impl GamepadList {
fn new_inherited(list: &[&Gamepad]) -> GamepadList {
GamepadList {
@ -46,6 +44,10 @@ impl GamepadList {
}
}
}
pub fn remove_gamepad(&self, index: usize) {
self.list.borrow_mut().remove(index);
}
}
impl GamepadListMethods for GamepadList {

View file

@ -51,8 +51,8 @@ use profile_traits::{ipc as profile_ipc, mem as profile_mem, time as profile_tim
use script_traits::serializable::{BlobData, BlobImpl, FileBlob};
use script_traits::transferable::MessagePortImpl;
use script_traits::{
BroadcastMsg, MessagePortMsg, MsDuration, PortMessageTask, ScriptMsg,
ScriptToConstellationChan, TimerEvent, TimerEventId, TimerSchedulerMsg, TimerSource,
BroadcastMsg, GamepadEvent, GamepadUpdateType, MessagePortMsg, MsDuration, PortMessageTask,
ScriptMsg, ScriptToConstellationChan, TimerEvent, TimerEventId, TimerSchedulerMsg, TimerSource,
};
use servo_url::{ImmutableOrigin, MutableOrigin, ServoUrl};
use uuid::Uuid;
@ -63,9 +63,12 @@ use super::bindings::trace::HashMapTracedValues;
use crate::dom::bindings::cell::{DomRefCell, RefMut};
use crate::dom::bindings::codegen::Bindings::BroadcastChannelBinding::BroadcastChannelMethods;
use crate::dom::bindings::codegen::Bindings::EventSourceBinding::EventSource_Binding::EventSourceMethods;
use crate::dom::bindings::codegen::Bindings::GamepadListBinding::GamepadList_Binding::GamepadListMethods;
use crate::dom::bindings::codegen::Bindings::ImageBitmapBinding::{
ImageBitmapOptions, ImageBitmapSource,
};
use crate::dom::bindings::codegen::Bindings::NavigatorBinding::Navigator_Binding::NavigatorMethods;
use crate::dom::bindings::codegen::Bindings::PerformanceBinding::Performance_Binding::PerformanceMethods;
use crate::dom::bindings::codegen::Bindings::PermissionStatusBinding::PermissionState;
use crate::dom::bindings::codegen::Bindings::VoidFunctionBinding::VoidFunction;
use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
@ -92,6 +95,7 @@ use crate::dom::event::{Event, EventBubbles, EventCancelable, EventStatus};
use crate::dom::eventsource::EventSource;
use crate::dom::eventtarget::EventTarget;
use crate::dom::file::File;
use crate::dom::gamepad::Gamepad;
use crate::dom::gpudevice::GPUDevice;
use crate::dom::htmlscriptelement::{ScriptId, SourceCode};
use crate::dom::identityhub::Identities;
@ -118,6 +122,7 @@ use crate::script_thread::{MainThreadScriptChan, ScriptThread};
use crate::task::TaskCanceller;
use crate::task_source::dom_manipulation::DOMManipulationTaskSource;
use crate::task_source::file_reading::FileReadingTaskSource;
use crate::task_source::gamepad::GamepadTaskSource;
use crate::task_source::networking::NetworkingTaskSource;
use crate::task_source::performance_timeline::PerformanceTimelineTaskSource;
use crate::task_source::port_message::PortMessageQueue;
@ -2517,6 +2522,16 @@ impl GlobalScope {
unreachable!();
}
/// `TaskSource` to send messages to the gamepad task source of
/// this global scope.
/// <https://w3c.github.io/gamepad/#dfn-gamepad-task-source>
pub fn gamepad_task_source(&self) -> GamepadTaskSource {
if let Some(window) = self.downcast::<Window>() {
return window.task_manager().gamepad_task_source();
}
unreachable!();
}
/// `TaskSource` to send messages to the networking task source of
/// this global scope.
pub fn networking_task_source(&self) -> NetworkingTaskSource {
@ -3091,6 +3106,121 @@ impl GlobalScope {
.handle_server_msg(scope, result);
}
pub fn handle_gamepad_event(&self, gamepad_event: GamepadEvent) {
match gamepad_event {
GamepadEvent::Connected(index, name, bounds) => {
self.handle_gamepad_connect(
index.0,
name,
bounds.axis_bounds,
bounds.button_bounds,
);
},
GamepadEvent::Disconnected(index) => {
self.handle_gamepad_disconnect(index.0);
},
GamepadEvent::Updated(index, update_type) => {
self.receive_new_gamepad_button_or_axis(index.0, update_type);
},
};
}
/// <https://www.w3.org/TR/gamepad/#dfn-gamepadconnected>
pub fn handle_gamepad_connect(
&self,
index: usize,
name: String,
axis_bounds: (f64, f64),
button_bounds: (f64, f64),
) {
// TODO: 2. If document is not null and is not allowed to use the "gamepad" permission,
// then abort these steps.
let this = Trusted::new(&*self);
self.gamepad_task_source().queue(
task!(gamepad_connected: move || {
let global = this.root();
let gamepad = Gamepad::new(&global, index as u32, name, axis_bounds, button_bounds);
if let Some(window) = global.downcast::<Window>() {
let gamepad_list = window.Navigator().GetGamepads();
let gamepad_arr: [DomRoot<Gamepad>; 1] = [gamepad.clone()];
gamepad_list.add_if_not_exists(&gamepad_arr);
// TODO: 3.4 If navigator.[[hasGamepadGesture]] is true:
// TODO: 3.4.1 Set gamepad.[[exposed]] to true.
if window.Document().is_fully_active() {
gamepad.update_connected(true);
}
}
}),
&self,
)
.unwrap();
}
/// <https://www.w3.org/TR/gamepad/#dfn-gamepaddisconnected>
pub fn handle_gamepad_disconnect(&self, index: usize) {
let this = Trusted::new(&*self);
self.gamepad_task_source()
.queue(
task!(gamepad_disconnected: move || {
let global = this.root();
if let Some(window) = global.downcast::<Window>() {
let gamepad_list = window.Navigator().GetGamepads();
if let Some(gamepad) = gamepad_list.Item(index as u32) {
// TODO: If gamepad.[[exposed]]
gamepad.update_connected(false);
gamepad_list.remove_gamepad(index);
}
for i in (0..gamepad_list.Length()).rev() {
if gamepad_list.Item(i as u32).is_none() {
gamepad_list.remove_gamepad(i as usize);
} else {
break;
}
}
}
}),
&self,
)
.unwrap();
}
/// <https://www.w3.org/TR/gamepad/#receiving-inputs>
pub fn receive_new_gamepad_button_or_axis(&self, index: usize, update_type: GamepadUpdateType) {
let this = Trusted::new(&*self);
// <https://w3c.github.io/gamepad/#dfn-update-gamepad-state>
self.gamepad_task_source()
.queue(
task!(update_gamepad_state: move || {
let global = this.root();
if let Some(window) = global.downcast::<Window>() {
let gamepad_list = window.Navigator().GetGamepads();
if let Some(gamepad) = gamepad_list.IndexedGetter(index as u32) {
let current_time = global.performance().Now();
gamepad.update_timestamp(*current_time);
match update_type {
GamepadUpdateType::Axis(index, value) => {
gamepad.map_and_normalize_axes(index, value);
},
GamepadUpdateType::Button(index, value) => {
gamepad.map_and_normalize_buttons(index, value);
}
};
// TODO: 6. If navigator.[[hasGamepadGesture]] is false
// and gamepad contains a gamepad user gesture:
}
}
}),
&self,
)
.unwrap();
}
pub(crate) fn current_group_label(&self) -> Option<DOMString> {
self.console_group_stack
.borrow()

View file

@ -515,6 +515,8 @@ macro_rules! window_event_handlers(
event_handler!(unhandledrejection, GetOnunhandledrejection,
SetOnunhandledrejection);
event_handler!(unload, GetOnunload, SetOnunload);
event_handler!(gamepadconnected, GetOngamepadconnected, SetOngamepadconnected);
event_handler!(gamepaddisconnected, GetOngamepaddisconnected, SetOngamepaddisconnected);
);
(ForwardToWindow) => (
window_owned_event_handler!(afterprint, GetOnafterprint,
@ -541,6 +543,8 @@ macro_rules! window_event_handlers(
window_owned_event_handler!(unhandledrejection, GetOnunhandledrejection,
SetOnunhandledrejection);
window_owned_event_handler!(unload, GetOnunload, SetOnunload);
window_owned_event_handler!(gamepadconnected, GetOngamepadconnected, SetOngamepadconnected);
window_owned_event_handler!(gamepaddisconnected, GetOngamepaddisconnected, SetOngamepaddisconnected);
);
);

View file

@ -27,3 +27,9 @@ enum GamepadHand {
"left",
"right"
};
// https://www.w3.org/TR/gamepad/#extensions-to-the-windoweventhandlers-interface-mixin
partial interface mixin WindowEventHandlers {
attribute EventHandler ongamepadconnected;
attribute EventHandler ongamepaddisconnected;
};

View file

@ -75,8 +75,8 @@ use profile_traits::time::{self as profile_time, profile, ProfilerCategory};
use script_layout_interface::message::{self, LayoutThreadInit, Msg, ReflowGoal};
use script_traits::webdriver_msg::WebDriverScriptCommand;
use script_traits::CompositorEvent::{
CompositionEvent, IMEDismissedEvent, KeyboardEvent, MouseButtonEvent, MouseMoveEvent,
ResizeEvent, TouchEvent, WheelEvent,
CompositionEvent, GamepadEvent, IMEDismissedEvent, KeyboardEvent, MouseButtonEvent,
MouseMoveEvent, ResizeEvent, TouchEvent, WheelEvent,
};
use script_traits::{
AnimationTickType, CompositorEvent, ConstellationControlMsg, DiscardBrowsingContext,
@ -152,6 +152,7 @@ use crate::task_manager::TaskManager;
use crate::task_queue::{QueuedTask, QueuedTaskConversion, TaskQueue};
use crate::task_source::dom_manipulation::DOMManipulationTaskSource;
use crate::task_source::file_reading::FileReadingTaskSource;
use crate::task_source::gamepad::GamepadTaskSource;
use crate::task_source::history_traversal::HistoryTraversalTaskSource;
use crate::task_source::media_element::MediaElementTaskSource;
use crate::task_source::networking::NetworkingTaskSource;
@ -568,6 +569,8 @@ pub struct ScriptThread {
dom_manipulation_task_sender: Box<dyn ScriptChan>,
gamepad_task_sender: Box<dyn ScriptChan>,
#[no_trace]
media_element_task_sender: Sender<MainThreadScriptMsg>,
@ -1354,6 +1357,7 @@ impl ScriptThread {
chan: MainThreadScriptChan(chan.clone()),
dom_manipulation_task_sender: boxed_script_sender.clone(),
gamepad_task_sender: boxed_script_sender.clone(),
media_element_task_sender: chan.clone(),
user_interaction_task_sender: chan.clone(),
networking_task_sender: boxed_script_sender.clone(),
@ -2858,6 +2862,10 @@ impl ScriptThread {
DOMManipulationTaskSource(self.dom_manipulation_task_sender.clone(), pipeline_id)
}
pub fn gamepad_task_source(&self, pipeline_id: PipelineId) -> GamepadTaskSource {
GamepadTaskSource(self.gamepad_task_sender.clone(), pipeline_id)
}
pub fn media_element_task_source(&self, pipeline_id: PipelineId) -> MediaElementTaskSource {
MediaElementTaskSource(self.media_element_task_sender.clone(), pipeline_id)
}
@ -3283,6 +3291,7 @@ impl ScriptThread {
let task_manager = TaskManager::new(
self.dom_manipulation_task_source(incomplete.pipeline_id),
self.file_reading_task_source(incomplete.pipeline_id),
self.gamepad_task_source(incomplete.pipeline_id),
self.history_traversal_task_source(incomplete.pipeline_id),
self.media_element_task_source(incomplete.pipeline_id),
self.networking_task_source(incomplete.pipeline_id),
@ -3669,6 +3678,15 @@ impl ScriptThread {
};
document.dispatch_composition_event(composition_event);
},
GamepadEvent(gamepad_event) => {
let window = match self.documents.borrow().find_window(pipeline_id) {
Some(window) => window,
None => return warn!("Message sent to closed pipeline {}.", pipeline_id),
};
let global = window.upcast::<GlobalScope>();
global.handle_gamepad_event(gamepad_event);
},
}
ScriptThread::set_user_interacting(false);

View file

@ -10,6 +10,7 @@ use crate::dom::bindings::cell::DomRefCell;
use crate::task::TaskCanceller;
use crate::task_source::dom_manipulation::DOMManipulationTaskSource;
use crate::task_source::file_reading::FileReadingTaskSource;
use crate::task_source::gamepad::GamepadTaskSource;
use crate::task_source::history_traversal::HistoryTraversalTaskSource;
use crate::task_source::media_element::MediaElementTaskSource;
use crate::task_source::networking::NetworkingTaskSource;
@ -42,6 +43,8 @@ pub struct TaskManager {
#[ignore_malloc_size_of = "task sources are hard"]
file_reading_task_source: FileReadingTaskSource,
#[ignore_malloc_size_of = "task sources are hard"]
gamepad_task_source: GamepadTaskSource,
#[ignore_malloc_size_of = "task sources are hard"]
history_traversal_task_source: HistoryTraversalTaskSource,
#[ignore_malloc_size_of = "task sources are hard"]
media_element_task_source: MediaElementTaskSource,
@ -65,6 +68,7 @@ impl TaskManager {
pub fn new(
dom_manipulation_task_source: DOMManipulationTaskSource,
file_reading_task_source: FileReadingTaskSource,
gamepad_task_source: GamepadTaskSource,
history_traversal_task_source: HistoryTraversalTaskSource,
media_element_task_source: MediaElementTaskSource,
networking_task_source: NetworkingTaskSource,
@ -78,6 +82,7 @@ impl TaskManager {
TaskManager {
dom_manipulation_task_source,
file_reading_task_source,
gamepad_task_source,
history_traversal_task_source,
media_element_task_source,
networking_task_source,
@ -99,6 +104,14 @@ impl TaskManager {
DOMManipulation
);
task_source_functions!(
self,
gamepad_task_source_with_canceller,
gamepad_task_source,
GamepadTaskSource,
Gamepad
);
task_source_functions!(
self,
media_element_task_source_with_canceller,

View file

@ -0,0 +1,47 @@
/* 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 http://mozilla.org/MPL/2.0/. */
use std::fmt;
use std::result::Result;
use msg::constellation_msg::PipelineId;
use crate::script_runtime::{CommonScriptMsg, ScriptChan, ScriptThreadEventCategory};
use crate::task::{TaskCanceller, TaskOnce};
use crate::task_source::{TaskSource, TaskSourceName};
#[derive(JSTraceable)]
pub struct GamepadTaskSource(
pub Box<dyn ScriptChan + Send + 'static>,
#[no_trace] pub PipelineId,
);
impl Clone for GamepadTaskSource {
fn clone(&self) -> GamepadTaskSource {
GamepadTaskSource(self.0.clone(), self.1.clone())
}
}
impl fmt::Debug for GamepadTaskSource {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "GamepadTaskSource(...)")
}
}
impl TaskSource for GamepadTaskSource {
const NAME: TaskSourceName = TaskSourceName::Gamepad;
fn queue_with_canceller<T>(&self, task: T, canceller: &TaskCanceller) -> Result<(), ()>
where
T: TaskOnce + 'static,
{
let msg = CommonScriptMsg::Task(
ScriptThreadEventCategory::InputEvent,
Box::new(canceller.wrap_task(task)),
Some(self.1),
GamepadTaskSource::NAME,
);
self.0.send(msg).map_err(|_| ())
}
}

View file

@ -4,6 +4,7 @@
pub mod dom_manipulation;
pub mod file_reading;
pub mod gamepad;
pub mod history_traversal;
pub mod media_element;
pub mod networking;
@ -38,6 +39,8 @@ pub enum TaskSourceName {
MediaElement,
Websocket,
Timer,
/// <https://www.w3.org/TR/gamepad/#dfn-gamepad-task-source>
Gamepad,
}
impl TaskSourceName {

View file

@ -791,6 +791,13 @@ where
);
}
},
EmbedderEvent::Gamepad(gamepad_event) => {
let msg = ConstellationMsg::Gamepad(gamepad_event);
if let Err(e) = self.constellation_chan.send(msg) {
warn!("Sending Gamepad event to constellation failed ({:?}).", e);
}
},
}
return false;
}

View file

@ -14,8 +14,8 @@ use msg::constellation_msg::{
BrowsingContextId, PipelineId, TopLevelBrowsingContextId, TraversalDirection,
};
use script_traits::{
AnimationTickType, CompositorEvent, LogEntry, MediaSessionActionType, WebDriverCommandMsg,
WindowSizeData, WindowSizeType,
AnimationTickType, CompositorEvent, GamepadEvent, LogEntry, MediaSessionActionType,
WebDriverCommandMsg, WindowSizeData, WindowSizeType,
};
use servo_url::ServoUrl;
@ -82,6 +82,8 @@ pub enum ConstellationMsg {
IMEDismissed,
/// Compositing done, but external code needs to present.
ReadyToPresent(TopLevelBrowsingContextId),
/// Gamepad state has changed
Gamepad(GamepadEvent),
}
impl fmt::Debug for ConstellationMsg {
@ -117,6 +119,7 @@ impl fmt::Debug for ConstellationMsg {
IMEDismissed => "IMEDismissed",
ClearCache => "ClearCache",
ReadyToPresent(..) => "ReadyToPresent",
Gamepad(..) => "Gamepad",
};
write!(formatter, "ConstellationMsg::{}", variant)
}

View file

@ -227,6 +227,7 @@ pub enum CompositorEventVariant {
KeyboardEvent,
CompositionEvent,
IMEDismissedEvent,
GamepadEvent,
}
impl Debug for EmbedderMsg {

View file

@ -569,6 +569,8 @@ pub enum CompositorEvent {
CompositionEvent(CompositionEvent),
/// Virtual keyboard was dismissed
IMEDismissedEvent,
/// Connected gamepad state updated
GamepadEvent(GamepadEvent),
}
impl From<&CompositorEvent> for CompositorEventVariant {
@ -582,6 +584,7 @@ impl From<&CompositorEvent> for CompositorEventVariant {
CompositorEvent::KeyboardEvent(..) => CompositorEventVariant::KeyboardEvent,
CompositorEvent::CompositionEvent(..) => CompositorEventVariant::CompositionEvent,
CompositorEvent::IMEDismissedEvent => CompositorEventVariant::IMEDismissedEvent,
CompositorEvent::GamepadEvent(..) => CompositorEventVariant::GamepadEvent,
}
}
}
@ -1327,3 +1330,43 @@ impl SerializedImageData {
}
}
}
#[derive(
Clone, Copy, Debug, Deserialize, Eq, Hash, MallocSizeOf, Ord, PartialEq, PartialOrd, Serialize,
)]
/// Index of gamepad in list of system's connected gamepads
pub struct GamepadIndex(pub usize);
#[derive(Clone, Debug, Deserialize, Serialize)]
/// The minimum and maximum values that can be reported for axis or button input from this gamepad
pub struct GamepadInputBounds {
/// Minimum and maximum axis values
pub axis_bounds: (f64, f64),
/// Minimum and maximum button values
pub button_bounds: (f64, f64),
}
#[derive(Clone, Debug, Deserialize, Serialize)]
/// The type of Gamepad event
pub enum GamepadEvent {
/// A new gamepad has been connected
/// <https://www.w3.org/TR/gamepad/#event-gamepadconnected>
Connected(GamepadIndex, String, GamepadInputBounds),
/// An existing gamepad has been disconnected
/// <https://www.w3.org/TR/gamepad/#event-gamepaddisconnected>
Disconnected(GamepadIndex),
/// An existing gamepad has been updated
/// <https://www.w3.org/TR/gamepad/#receiving-inputs>
Updated(GamepadIndex, GamepadUpdateType),
}
#[derive(Clone, Debug, Deserialize, Serialize)]
/// The type of Gamepad input being updated
pub enum GamepadUpdateType {
/// Axis index and input value
/// <https://www.w3.org/TR/gamepad/#dfn-represents-a-standard-gamepad-axis>
Axis(usize, f64),
/// Button index and input value
/// <https://www.w3.org/TR/gamepad/#dfn-represents-a-standard-gamepad-button
Button(usize, f64),
}

View file

@ -52,6 +52,7 @@ egui_glow = { version = "0.22.0", features = ["winit"] }
egui-winit = { version = "0.22.0", default-features = false, features = ["clipboard", "wayland"] }
euclid = { workspace = true }
getopts = { workspace = true }
gilrs = "0.10.4"
gleam = { workspace = true }
glow = "0.12.2"
keyboard-types = { workspace = true }

View file

@ -438,6 +438,9 @@ impl App {
// Catch some keyboard events, and push the rest onto the WebViewManager event queue.
webviews.handle_window_events(embedder_events);
if webviews.webview_id().is_some() {
webviews.handle_gamepad_events();
}
// Take any new embedder messages from Servo itself.
let mut embedder_messages = self.servo.as_mut().unwrap().get_events();

View file

@ -12,6 +12,7 @@ use std::{env, thread};
use arboard::Clipboard;
use euclid::{Point2D, Vector2D};
use gilrs::{EventType, Gilrs};
use keyboard_types::{Key, KeyboardEvent, Modifiers, ShortcutMatcher};
use log::{debug, error, info, trace, warn};
use servo::compositing::windowing::{EmbedderEvent, WebRenderDebugOption};
@ -20,7 +21,9 @@ use servo::embedder_traits::{
PermissionRequest, PromptDefinition, PromptOrigin, PromptResult,
};
use servo::msg::constellation_msg::{TopLevelBrowsingContextId as WebViewId, TraversalDirection};
use servo::script_traits::TouchEventType;
use servo::script_traits::{
GamepadEvent, GamepadIndex, GamepadInputBounds, GamepadUpdateType, TouchEventType,
};
use servo::servo_config::opts;
use servo::servo_url::ServoUrl;
use servo::webrender_api::ScrollLocation;
@ -51,6 +54,7 @@ pub struct WebViewManager<Window: WindowPortsMethods + ?Sized> {
window: Rc<Window>,
event_queue: Vec<EmbedderEvent>,
clipboard: Option<Clipboard>,
gamepad: Option<Gilrs>,
shutdown_requested: bool,
}
@ -82,6 +86,13 @@ where
None
},
},
gamepad: match Gilrs::new() {
Ok(g) => Some(g),
Err(e) => {
warn!("Error creating gamepad input connection ({})", e);
None
},
},
event_queue: Vec::new(),
shutdown_requested: false,
}
@ -113,6 +124,109 @@ where
}
}
/// Handle updates to connected gamepads from GilRs
pub fn handle_gamepad_events(&mut self) {
if let Some(ref mut gilrs) = self.gamepad {
while let Some(event) = gilrs.next_event() {
let gamepad = gilrs.gamepad(event.id);
let name = gamepad.name();
let index = GamepadIndex(event.id.into());
match event.event {
EventType::ButtonPressed(button, _) => {
let mapped_index = Self::map_gamepad_button(button);
// We only want to send this for a valid digital button, aka on/off only
if !matches!(mapped_index, 6 | 7 | 17) {
let update_type = GamepadUpdateType::Button(mapped_index, 1.0);
let event = GamepadEvent::Updated(index, update_type);
self.event_queue.push(EmbedderEvent::Gamepad(event));
}
},
EventType::ButtonReleased(button, _) => {
let mapped_index = Self::map_gamepad_button(button);
// We only want to send this for a valid digital button, aka on/off only
if !matches!(mapped_index, 6 | 7 | 17) {
let update_type = GamepadUpdateType::Button(mapped_index, 0.0);
let event = GamepadEvent::Updated(index, update_type);
self.event_queue.push(EmbedderEvent::Gamepad(event));
}
},
EventType::ButtonChanged(button, value, _) => {
let mapped_index = Self::map_gamepad_button(button);
// We only want to send this for a valid non-digital button, aka the triggers
if matches!(mapped_index, 6 | 7) {
let update_type = GamepadUpdateType::Button(mapped_index, value as f64);
let event = GamepadEvent::Updated(index, update_type);
self.event_queue.push(EmbedderEvent::Gamepad(event));
}
},
EventType::AxisChanged(axis, value, _) => {
// Map axis index and value to represent Standard Gamepad axis
// <https://www.w3.org/TR/gamepad/#dfn-represents-a-standard-gamepad-axis>
let mapped_axis: usize = match axis {
gilrs::Axis::LeftStickX => 0,
gilrs::Axis::LeftStickY => 1,
gilrs::Axis::RightStickX => 2,
gilrs::Axis::RightStickY => 3,
_ => 4, // Other axes do not map to "standard" gamepad mapping and are ignored
};
if mapped_axis < 4 {
// The Gamepad spec designates down as positive and up as negative.
// GilRs does the inverse of this, so correct for it here.
let axis_value = match mapped_axis {
0 | 2 => value,
1 | 3 => -value,
_ => 0., // Should not reach here
};
let update_type =
GamepadUpdateType::Axis(mapped_axis, axis_value as f64);
let event = GamepadEvent::Updated(index, update_type);
self.event_queue.push(EmbedderEvent::Gamepad(event));
}
},
EventType::Connected => {
let name = String::from(name);
let bounds = GamepadInputBounds {
axis_bounds: (-1.0, 1.0),
button_bounds: (0.0, 1.0),
};
let event = GamepadEvent::Connected(index, name, bounds);
self.event_queue.push(EmbedderEvent::Gamepad(event));
},
EventType::Disconnected => {
let event = GamepadEvent::Disconnected(index);
self.event_queue.push(EmbedderEvent::Gamepad(event));
},
_ => {},
}
}
}
}
// Map button index and value to represent Standard Gamepad button
// <https://www.w3.org/TR/gamepad/#dfn-represents-a-standard-gamepad-button>
fn map_gamepad_button(button: gilrs::Button) -> usize {
match button {
gilrs::Button::South => 0,
gilrs::Button::East => 1,
gilrs::Button::West => 2,
gilrs::Button::North => 3,
gilrs::Button::LeftTrigger => 4,
gilrs::Button::RightTrigger => 5,
gilrs::Button::LeftTrigger2 => 6,
gilrs::Button::RightTrigger2 => 7,
gilrs::Button::Select => 8,
gilrs::Button::Start => 9,
gilrs::Button::LeftThumb => 10,
gilrs::Button::RightThumb => 11,
gilrs::Button::DPadUp => 12,
gilrs::Button::DPadDown => 13,
gilrs::Button::DPadLeft => 14,
gilrs::Button::DPadRight => 15,
gilrs::Button::Mode => 16,
_ => 17, // Other buttons do not map to "standard" gamepad mapping and are ignored
}
}
pub fn shutdown_requested(&self) -> bool {
self.shutdown_requested
}

View file

@ -1 +1 @@
prefs: ["dom.gamepad.enabled:true"]
prefs: [dom.gamepad.enabled:true]

View file

@ -8,24 +8,6 @@
[Stringification of new GamepadEvent("gamepad")]
expected: FAIL
[HTMLBodyElement interface: attribute ongamepadconnected]
expected: FAIL
[HTMLBodyElement interface: attribute ongamepaddisconnected]
expected: FAIL
[Window interface: attribute ongamepadconnected]
expected: FAIL
[Window interface: attribute ongamepaddisconnected]
expected: FAIL
[HTMLFrameSetElement interface: attribute ongamepadconnected]
expected: FAIL
[HTMLFrameSetElement interface: attribute ongamepaddisconnected]
expected: FAIL
[Gamepad interface: attribute vibrationActuator]
expected: FAIL

View file

@ -0,0 +1 @@
prefs: [dom.gamepad.enabled: true]

View file

@ -4,9 +4,3 @@
[Feature-Policy allow="gamepad" disallows cross-origin by default.]
expected: FAIL
[Feature-Policy allow="gamepad" allows same-origin by default.]
expected: FAIL
[Feature-Policy allow="gamepad" allows cross-origin by default.]
expected: FAIL

View file

@ -70,51 +70,3 @@
[Gamepad interface: attribute vibrationActuator]
expected: FAIL
[GamepadPose interface: existence and properties of interface object]
expected: FAIL
[GamepadPose interface object length]
expected: FAIL
[GamepadPose interface object name]
expected: FAIL
[GamepadPose interface: existence and properties of interface prototype object]
expected: FAIL
[GamepadPose interface: existence and properties of interface prototype object's "constructor" property]
expected: FAIL
[GamepadPose interface: existence and properties of interface prototype object's @@unscopables property]
expected: FAIL
[GamepadPose interface: attribute hasOrientation]
expected: FAIL
[GamepadPose interface: attribute hasPosition]
expected: FAIL
[GamepadPose interface: attribute position]
expected: FAIL
[GamepadPose interface: attribute linearVelocity]
expected: FAIL
[GamepadPose interface: attribute linearAcceleration]
expected: FAIL
[GamepadPose interface: attribute orientation]
expected: FAIL
[GamepadPose interface: attribute angularVelocity]
expected: FAIL
[GamepadPose interface: attribute angularAcceleration]
expected: FAIL
[Gamepad interface: attribute hand]
expected: FAIL
[Gamepad interface: attribute pose]
expected: FAIL

View file

@ -8,117 +8,6 @@
[Stringification of new GamepadEvent("gamepad")]
expected: FAIL
[HTMLBodyElement interface: attribute ongamepadconnected]
expected: FAIL
[HTMLBodyElement interface: attribute ongamepaddisconnected]
expected: FAIL
[Window interface: attribute ongamepadconnected]
expected: FAIL
[Window interface: attribute ongamepaddisconnected]
expected: FAIL
[HTMLFrameSetElement interface: attribute ongamepadconnected]
expected: FAIL
[HTMLFrameSetElement interface: attribute ongamepaddisconnected]
expected: FAIL
[Gamepad interface: existence and properties of interface object]
expected: FAIL
[Gamepad interface object length]
expected: FAIL
[Gamepad interface object name]
expected: FAIL
[Gamepad interface: existence and properties of interface prototype object]
expected: FAIL
[Gamepad interface: existence and properties of interface prototype object's "constructor" property]
expected: FAIL
[Gamepad interface: existence and properties of interface prototype object's @@unscopables property]
expected: FAIL
[Gamepad interface: attribute id]
expected: FAIL
[Gamepad interface: attribute index]
expected: FAIL
[Gamepad interface: attribute connected]
expected: FAIL
[Gamepad interface: attribute timestamp]
expected: FAIL
[Gamepad interface: attribute mapping]
expected: FAIL
[Gamepad interface: attribute axes]
expected: FAIL
[Gamepad interface: attribute buttons]
expected: FAIL
[GamepadButton interface: existence and properties of interface object]
expected: FAIL
[GamepadButton interface object length]
expected: FAIL
[GamepadButton interface object name]
expected: FAIL
[GamepadButton interface: existence and properties of interface prototype object]
expected: FAIL
[GamepadButton interface: existence and properties of interface prototype object's "constructor" property]
expected: FAIL
[GamepadButton interface: existence and properties of interface prototype object's @@unscopables property]
expected: FAIL
[GamepadButton interface: attribute pressed]
expected: FAIL
[GamepadButton interface: attribute touched]
expected: FAIL
[GamepadButton interface: attribute value]
expected: FAIL
[GamepadEvent interface: existence and properties of interface object]
expected: FAIL
[GamepadEvent interface object length]
expected: FAIL
[GamepadEvent interface object name]
expected: FAIL
[GamepadEvent interface: existence and properties of interface prototype object]
expected: FAIL
[GamepadEvent interface: existence and properties of interface prototype object's "constructor" property]
expected: FAIL
[GamepadEvent interface: existence and properties of interface prototype object's @@unscopables property]
expected: FAIL
[GamepadEvent interface: attribute gamepad]
expected: FAIL
[Navigator interface: operation getGamepads()]
expected: FAIL
[Navigator interface: navigator must inherit property "getGamepads()" with the proper type]
expected: FAIL
[Gamepad interface: attribute vibrationActuator]
expected: FAIL