diff --git a/Cargo.lock b/Cargo.lock index d037315c2bd..9a2ae2eb98d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/components/compositing/windowing.rs b/components/compositing/windowing.rs index 98e610d9921..a1a36ee688d 100644 --- a/components/compositing/windowing.rs +++ b/components/compositing/windowing.rs @@ -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"), } } } diff --git a/components/constellation/constellation.rs b/components/constellation/constellation.rs index cfc6ced6cb3..e7ddb5a95f5 100644 --- a/components/constellation/constellation.rs +++ b/components/constellation/constellation.rs @@ -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"); + }, + } + } } diff --git a/components/script/dom/gamepad.rs b/components/script/dom/gamepad.rs index b0e7f04b818..c25db0747ca 100644 --- a/components/script/dom/gamepad.rs +++ b/components/script/dom/gamepad.rs @@ -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>, #[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 { + 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 + /// + fn new_with_proto( + global: &GlobalScope, + gamepad_id: u32, + id: String, + axis_bounds: (f64, f64), + button_bounds: (f64, f64), + ) -> DomRoot { + 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::() .fire(self.global().as_window().upcast::()); } + + /// Initialize the number of axes in the "standard" gamepad mapping. + /// + fn init_axes(&self) { + let initial_axes: Vec = 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)] + /// + 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!"); + } + } + + /// + 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!"); + } + } } diff --git a/components/script/dom/gamepadbutton.rs b/components/script/dom/gamepadbutton.rs index a739dbebcf4..c23b2848379 100644 --- a/components/script/dom/gamepadbutton.rs +++ b/components/script/dom/gamepadbutton.rs @@ -20,8 +20,6 @@ pub struct GamepadButton { value: Cell, } -// 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); } } diff --git a/components/script/dom/gamepadbuttonlist.rs b/components/script/dom/gamepadbuttonlist.rs index e28e33e05a8..961f265e37f 100644 --- a/components/script/dom/gamepadbuttonlist.rs +++ b/components/script/dom/gamepadbuttonlist.rs @@ -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>, } -// 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 { + 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. + /// + pub fn init_buttons(global: &GlobalScope) -> DomRoot { + 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()) + } +} diff --git a/components/script/dom/gamepadlist.rs b/components/script/dom/gamepadlist.rs index d293281389b..4f373e24d96 100644 --- a/components/script/dom/gamepadlist.rs +++ b/components/script/dom/gamepadlist.rs @@ -18,8 +18,6 @@ pub struct GamepadList { list: DomRefCell>>, } -// 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 { diff --git a/components/script/dom/globalscope.rs b/components/script/dom/globalscope.rs index 00c19614014..cb8bb72a7fd 100644 --- a/components/script/dom/globalscope.rs +++ b/components/script/dom/globalscope.rs @@ -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. + /// + pub fn gamepad_task_source(&self) -> GamepadTaskSource { + if let Some(window) = self.downcast::() { + 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); + }, + }; + } + + /// + 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::() { + let gamepad_list = window.Navigator().GetGamepads(); + let gamepad_arr: [DomRoot; 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(); + } + + /// + 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::() { + 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(); + } + + /// + pub fn receive_new_gamepad_button_or_axis(&self, index: usize, update_type: GamepadUpdateType) { + let this = Trusted::new(&*self); + + // + self.gamepad_task_source() + .queue( + task!(update_gamepad_state: move || { + let global = this.root(); + if let Some(window) = global.downcast::() { + 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 { self.console_group_stack .borrow() diff --git a/components/script/dom/macros.rs b/components/script/dom/macros.rs index e9b48731280..fa8554cac28 100644 --- a/components/script/dom/macros.rs +++ b/components/script/dom/macros.rs @@ -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); ); ); diff --git a/components/script/dom/webidls/Gamepad.webidl b/components/script/dom/webidls/Gamepad.webidl index 925eaf2a544..306aa0c216b 100644 --- a/components/script/dom/webidls/Gamepad.webidl +++ b/components/script/dom/webidls/Gamepad.webidl @@ -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; +}; diff --git a/components/script/script_thread.rs b/components/script/script_thread.rs index 0bb89c0956b..2ac9b1f05f5 100644 --- a/components/script/script_thread.rs +++ b/components/script/script_thread.rs @@ -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, + gamepad_task_sender: Box, + #[no_trace] media_element_task_sender: Sender, @@ -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::(); + global.handle_gamepad_event(gamepad_event); + }, } ScriptThread::set_user_interacting(false); diff --git a/components/script/task_manager.rs b/components/script/task_manager.rs index a64355b6882..a732f8d5dda 100644 --- a/components/script/task_manager.rs +++ b/components/script/task_manager.rs @@ -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, diff --git a/components/script/task_source/gamepad.rs b/components/script/task_source/gamepad.rs new file mode 100644 index 00000000000..5adf519a5e8 --- /dev/null +++ b/components/script/task_source/gamepad.rs @@ -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, + #[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(&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(|_| ()) + } +} diff --git a/components/script/task_source/mod.rs b/components/script/task_source/mod.rs index b67b87c8caf..07776b7b801 100644 --- a/components/script/task_source/mod.rs +++ b/components/script/task_source/mod.rs @@ -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, + /// + Gamepad, } impl TaskSourceName { diff --git a/components/servo/lib.rs b/components/servo/lib.rs index 92278eab1b7..eb423ea4685 100644 --- a/components/servo/lib.rs +++ b/components/servo/lib.rs @@ -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; } diff --git a/components/shared/compositing/constellation_msg.rs b/components/shared/compositing/constellation_msg.rs index 12b74cb2f67..263d0f510a8 100644 --- a/components/shared/compositing/constellation_msg.rs +++ b/components/shared/compositing/constellation_msg.rs @@ -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) } diff --git a/components/shared/embedder/lib.rs b/components/shared/embedder/lib.rs index 0337884ea1a..a03993359c4 100644 --- a/components/shared/embedder/lib.rs +++ b/components/shared/embedder/lib.rs @@ -227,6 +227,7 @@ pub enum CompositorEventVariant { KeyboardEvent, CompositionEvent, IMEDismissedEvent, + GamepadEvent, } impl Debug for EmbedderMsg { diff --git a/components/shared/script/lib.rs b/components/shared/script/lib.rs index 325423a8013..7ade170fe77 100644 --- a/components/shared/script/lib.rs +++ b/components/shared/script/lib.rs @@ -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 + /// + Connected(GamepadIndex, String, GamepadInputBounds), + /// An existing gamepad has been disconnected + /// + Disconnected(GamepadIndex), + /// An existing gamepad has been updated + /// + Updated(GamepadIndex, GamepadUpdateType), +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +/// The type of Gamepad input being updated +pub enum GamepadUpdateType { + /// Axis index and input value + /// + Axis(usize, f64), + /// Button index and input value + /// { window: Rc, event_queue: Vec, clipboard: Option, + gamepad: Option, 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 + // + 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 + // + 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 } diff --git a/tests/wpt/meta-legacy-layout/gamepad/__dir__.ini b/tests/wpt/meta-legacy-layout/gamepad/__dir__.ini index 8d7c3380c26..7c5c7c7dd57 100644 --- a/tests/wpt/meta-legacy-layout/gamepad/__dir__.ini +++ b/tests/wpt/meta-legacy-layout/gamepad/__dir__.ini @@ -1 +1 @@ -prefs: ["dom.gamepad.enabled:true"] +prefs: [dom.gamepad.enabled:true] diff --git a/tests/wpt/meta-legacy-layout/gamepad/idlharness.https.window.js.ini b/tests/wpt/meta-legacy-layout/gamepad/idlharness.https.window.js.ini index 4032b480253..55ec1643e45 100644 --- a/tests/wpt/meta-legacy-layout/gamepad/idlharness.https.window.js.ini +++ b/tests/wpt/meta-legacy-layout/gamepad/idlharness.https.window.js.ini @@ -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 diff --git a/tests/wpt/meta/gamepad/__dir__.ini b/tests/wpt/meta/gamepad/__dir__.ini new file mode 100644 index 00000000000..18827c1dbe2 --- /dev/null +++ b/tests/wpt/meta/gamepad/__dir__.ini @@ -0,0 +1 @@ +prefs: [dom.gamepad.enabled: true] \ No newline at end of file diff --git a/tests/wpt/meta/gamepad/gamepad-default-feature-policy.https.sub.html.ini b/tests/wpt/meta/gamepad/gamepad-default-feature-policy.https.sub.html.ini index 678bf9112c2..40e488a878d 100644 --- a/tests/wpt/meta/gamepad/gamepad-default-feature-policy.https.sub.html.ini +++ b/tests/wpt/meta/gamepad/gamepad-default-feature-policy.https.sub.html.ini @@ -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 diff --git a/tests/wpt/meta/gamepad/idlharness-extensions.https.window.js.ini b/tests/wpt/meta/gamepad/idlharness-extensions.https.window.js.ini index e55d8fa4921..1904bd6929d 100644 --- a/tests/wpt/meta/gamepad/idlharness-extensions.https.window.js.ini +++ b/tests/wpt/meta/gamepad/idlharness-extensions.https.window.js.ini @@ -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 diff --git a/tests/wpt/meta/gamepad/idlharness.https.window.js.ini b/tests/wpt/meta/gamepad/idlharness.https.window.js.ini index c80c2cc585b..55ec1643e45 100644 --- a/tests/wpt/meta/gamepad/idlharness.https.window.js.ini +++ b/tests/wpt/meta/gamepad/idlharness.https.window.js.ini @@ -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