diff options
-rw-r--r-- | Cargo.lock | 10 | ||||
-rw-r--r-- | components/constellation/tracing.rs | 2 | ||||
-rw-r--r-- | components/script/dom/bindings/codegen/Bindings.conf | 4 | ||||
-rw-r--r-- | components/script/dom/document.rs | 13 | ||||
-rw-r--r-- | components/script/dom/gamepad.rs | 29 | ||||
-rw-r--r-- | components/script/dom/gamepadhapticactuator.rs | 388 | ||||
-rw-r--r-- | components/script/dom/globalscope.rs | 13 | ||||
-rw-r--r-- | components/script/dom/mod.rs | 1 | ||||
-rw-r--r-- | components/script/dom/webidls/Gamepad.webidl | 1 | ||||
-rw-r--r-- | components/script/dom/webidls/GamepadHapticActuator.webidl | 38 | ||||
-rw-r--r-- | components/shared/embedder/lib.rs | 21 | ||||
-rw-r--r-- | components/shared/script/lib.rs | 16 | ||||
-rw-r--r-- | ports/servoshell/Cargo.toml | 2 | ||||
-rw-r--r-- | ports/servoshell/desktop/tracing.rs | 2 | ||||
-rw-r--r-- | ports/servoshell/desktop/webview.rs | 128 | ||||
-rw-r--r-- | ports/servoshell/egl/servo_glue.rs | 4 | ||||
-rw-r--r-- | tests/wpt/meta-legacy-layout/gamepad/idlharness.window.js.ini | 24 | ||||
-rw-r--r-- | tests/wpt/meta/gamepad/idlharness.window.js.ini | 24 |
18 files changed, 652 insertions, 68 deletions
diff --git a/Cargo.lock b/Cargo.lock index 5f2ee3514f7..38e42340bc0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2130,9 +2130,8 @@ dependencies = [ [[package]] name = "gilrs" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f226b8f4d9bc7da93de8efd8747c6b1086409ca3f4b6d51e9a7f5461a9183fe" +version = "0.10.6" +source = "git+https://gitlab.com/gilrs-project/gilrs?rev=eafb7f2ef488874188c5d75adce9aef486be9d4e#eafb7f2ef488874188c5d75adce9aef486be9d4e" dependencies = [ "fnv", "gilrs-core", @@ -2143,9 +2142,8 @@ dependencies = [ [[package]] name = "gilrs-core" -version = "0.5.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb5e8d912059b33b463831c16b838d15c4772d584ce332e4a80f6dffdae2bc1" +version = "0.5.12" +source = "git+https://gitlab.com/gilrs-project/gilrs?rev=eafb7f2ef488874188c5d75adce9aef486be9d4e#eafb7f2ef488874188c5d75adce9aef486be9d4e" dependencies = [ "core-foundation", "inotify", diff --git a/components/constellation/tracing.rs b/components/constellation/tracing.rs index 509b6aa404f..46335012d03 100644 --- a/components/constellation/tracing.rs +++ b/components/constellation/tracing.rs @@ -237,6 +237,8 @@ mod from_script { Self::OnDevtoolsStarted(..) => target_variant!("OnDevtoolsStarted"), Self::ReadyToPresent(..) => target_variant!("ReadyToPresent"), Self::EventDelivered(..) => target_variant!("EventDelivered"), + Self::PlayGamepadHapticEffect(..) => target_variant!("PlayGamepadHapticEffect"), + Self::StopGamepadHapticEffect(..) => target_variant!("StopGamepadHapticEffect"), } } } diff --git a/components/script/dom/bindings/codegen/Bindings.conf b/components/script/dom/bindings/codegen/Bindings.conf index a03c2b0b77e..6024f6e6fd2 100644 --- a/components/script/dom/bindings/codegen/Bindings.conf +++ b/components/script/dom/bindings/codegen/Bindings.conf @@ -176,6 +176,10 @@ DOMInterfaces = { 'CreateRenderPipelineAsync', 'CreateShaderModule' # Creates promise for compilation info ], +}, + +'GamepadHapticActuator': { + 'inRealms': ['PlayEffect', 'Reset'] } } diff --git a/components/script/dom/document.rs b/components/script/dom/document.rs index 4f6d2ab0354..64c4b2eed7f 100644 --- a/components/script/dom/document.rs +++ b/components/script/dom/document.rs @@ -4125,6 +4125,19 @@ impl Document { // Step 6 Run any page visibility change steps which may be defined in other specifications, with visibility // state and document. Any other specs' visibility steps will go here. + // <https://www.w3.org/TR/gamepad/#handling-visibility-change> + if visibility_state == DocumentVisibilityState::Hidden { + self.window + .Navigator() + .GetGamepads() + .iter_mut() + .for_each(|gamepad| { + if let Some(g) = gamepad { + g.vibration_actuator().handle_visibility_change(); + } + }); + } + // Step 7 Fire an event named visibilitychange at document, with its bubbles attribute initialized to true. self.upcast::<EventTarget>() .fire_bubbling_event(atom!("visibilitychange")); diff --git a/components/script/dom/gamepad.rs b/components/script/dom/gamepad.rs index cee1077d00f..951be3d05d5 100644 --- a/components/script/dom/gamepad.rs +++ b/components/script/dom/gamepad.rs @@ -6,7 +6,7 @@ use std::cell::Cell; use dom_struct::dom_struct; use js::typedarray::{Float64, Float64Array}; -use script_traits::GamepadUpdateType; +use script_traits::{GamepadSupportedHapticEffects, GamepadUpdateType}; use super::bindings::buffer_source::HeapBufferSource; use crate::dom::bindings::codegen::Bindings::GamepadBinding::{GamepadHand, GamepadMethods}; @@ -20,6 +20,7 @@ use crate::dom::event::Event; use crate::dom::eventtarget::EventTarget; use crate::dom::gamepadbuttonlist::GamepadButtonList; use crate::dom::gamepadevent::{GamepadEvent, GamepadEventType}; +use crate::dom::gamepadhapticactuator::GamepadHapticActuator; use crate::dom::gamepadpose::GamepadPose; use crate::dom::globalscope::GlobalScope; use crate::script_runtime::JSContext; @@ -49,6 +50,7 @@ pub struct Gamepad { axis_bounds: (f64, f64), button_bounds: (f64, f64), exposed: Cell<bool>, + vibration_actuator: Dom<GamepadHapticActuator>, } impl Gamepad { @@ -65,6 +67,7 @@ impl Gamepad { hand: GamepadHand, axis_bounds: (f64, f64), button_bounds: (f64, f64), + vibration_actuator: &GamepadHapticActuator, ) -> Gamepad { Self { reflector_: Reflector::new(), @@ -81,6 +84,7 @@ impl Gamepad { axis_bounds, button_bounds, exposed: Cell::new(false), + vibration_actuator: Dom::from_ref(vibration_actuator), } } @@ -90,8 +94,16 @@ impl Gamepad { id: String, axis_bounds: (f64, f64), button_bounds: (f64, f64), + supported_haptic_effects: GamepadSupportedHapticEffects, ) -> DomRoot<Gamepad> { - Self::new_with_proto(global, gamepad_id, id, axis_bounds, button_bounds) + Self::new_with_proto( + global, + gamepad_id, + id, + axis_bounds, + button_bounds, + supported_haptic_effects, + ) } /// When we construct a new gamepad, we initialize the number of buttons and @@ -105,8 +117,11 @@ impl Gamepad { id: String, axis_bounds: (f64, f64), button_bounds: (f64, f64), + supported_haptic_effects: GamepadSupportedHapticEffects, ) -> DomRoot<Gamepad> { let button_list = GamepadButtonList::init_buttons(global); + let vibration_actuator = + GamepadHapticActuator::new(global, gamepad_id, supported_haptic_effects); let gamepad = reflect_dom_object_with_proto( Box::new(Gamepad::new_inherited( gamepad_id, @@ -120,6 +135,7 @@ impl Gamepad { GamepadHand::_empty, axis_bounds, button_bounds, + &vibration_actuator, )), global, None, @@ -165,6 +181,11 @@ impl GamepadMethods for Gamepad { DomRoot::from_ref(&*self.buttons) } + // https://w3c.github.io/gamepad/#dom-gamepad-vibrationactuator + fn VibrationActuator(&self) -> DomRoot<GamepadHapticActuator> { + DomRoot::from_ref(&*self.vibration_actuator) + } + // https://w3c.github.io/gamepad/extensions.html#gamepadhand-enum fn Hand(&self) -> GamepadHand { self.hand @@ -286,6 +307,10 @@ impl Gamepad { pub fn set_exposed(&self, exposed: bool) { self.exposed.set(exposed); } + + pub fn vibration_actuator(&self) -> &GamepadHapticActuator { + &*self.vibration_actuator + } } /// <https://www.w3.org/TR/gamepad/#dfn-gamepad-user-gesture> diff --git a/components/script/dom/gamepadhapticactuator.rs b/components/script/dom/gamepadhapticactuator.rs new file mode 100644 index 00000000000..d391c08d398 --- /dev/null +++ b/components/script/dom/gamepadhapticactuator.rs @@ -0,0 +1,388 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use std::cell::Cell; +use std::rc::Rc; + +use dom_struct::dom_struct; +use embedder_traits::{DualRumbleEffectParams, EmbedderMsg}; +use ipc_channel::ipc; +use ipc_channel::router::ROUTER; +use js::jsval::JSVal; +use script_traits::GamepadSupportedHapticEffects; + +use crate::dom::bindings::cell::DomRefCell; +use crate::dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods; +use crate::dom::bindings::codegen::Bindings::GamepadHapticActuatorBinding::{ + GamepadEffectParameters, GamepadHapticActuatorMethods, GamepadHapticEffectType, +}; +use crate::dom::bindings::codegen::Bindings::WindowBinding::Window_Binding::WindowMethods; +use crate::dom::bindings::error::Error; +use crate::dom::bindings::refcounted::{Trusted, TrustedPromise}; +use crate::dom::bindings::reflector::{reflect_dom_object_with_proto, DomObject, Reflector}; +use crate::dom::bindings::root::DomRoot; +use crate::dom::bindings::str::DOMString; +use crate::dom::bindings::utils::to_frozen_array; +use crate::dom::globalscope::GlobalScope; +use crate::dom::promise::Promise; +use crate::realms::{AlreadyInRealm, InRealm}; +use crate::script_runtime::JSContext; +use crate::task::TaskCanceller; +use crate::task_source::gamepad::GamepadTaskSource; +use crate::task_source::{TaskSource, TaskSourceName}; + +struct HapticEffectListener { + canceller: TaskCanceller, + task_source: GamepadTaskSource, + context: Trusted<GamepadHapticActuator>, +} + +impl HapticEffectListener { + fn handle_stopped(&self, stopped_successfully: bool) { + let context = self.context.clone(); + let _ = self.task_source.queue_with_canceller( + task!(handle_haptic_effect_stopped: move || { + let actuator = context.root(); + actuator.handle_haptic_effect_stopped(stopped_successfully); + }), + &self.canceller, + ); + } + + fn handle_completed(&self, completed_successfully: bool) { + let context = self.context.clone(); + let _ = self.task_source.queue_with_canceller( + task!(handle_haptic_effect_completed: move || { + let actuator = context.root(); + actuator.handle_haptic_effect_completed(completed_successfully); + }), + &self.canceller, + ); + } +} + +/// <https://www.w3.org/TR/gamepad/#gamepadhapticactuator-interface> +#[dom_struct] +pub struct GamepadHapticActuator { + reflector_: Reflector, + gamepad_index: u32, + /// <https://www.w3.org/TR/gamepad/#dfn-effects> + effects: Vec<GamepadHapticEffectType>, + /// <https://www.w3.org/TR/gamepad/#dfn-playingeffectpromise> + #[ignore_malloc_size_of = "Rc is hard"] + playing_effect_promise: DomRefCell<Option<Rc<Promise>>>, + /// The current sequence ID for playing effects, + /// incremented on every call to playEffect() or reset(). + /// Used to ensure that promises are resolved correctly. + /// Based on this pending PR <https://github.com/w3c/gamepad/pull/201> + sequence_id: Cell<u32>, + /// The sequence ID during the last playEffect() call + effect_sequence_id: Cell<u32>, + /// The sequence ID during the last reset() call + reset_sequence_id: Cell<u32>, +} + +impl GamepadHapticActuator { + fn new_inherited( + gamepad_index: u32, + supported_haptic_effects: GamepadSupportedHapticEffects, + ) -> GamepadHapticActuator { + let mut effects = vec![]; + if supported_haptic_effects.supports_dual_rumble { + effects.push(GamepadHapticEffectType::Dual_rumble); + } + if supported_haptic_effects.supports_trigger_rumble { + effects.push(GamepadHapticEffectType::Trigger_rumble); + } + Self { + reflector_: Reflector::new(), + gamepad_index: gamepad_index.into(), + effects, + playing_effect_promise: DomRefCell::new(None), + sequence_id: Cell::new(0), + effect_sequence_id: Cell::new(0), + reset_sequence_id: Cell::new(0), + } + } + + pub fn new( + global: &GlobalScope, + gamepad_index: u32, + supported_haptic_effects: GamepadSupportedHapticEffects, + ) -> DomRoot<GamepadHapticActuator> { + Self::new_with_proto(global, gamepad_index, supported_haptic_effects) + } + + fn new_with_proto( + global: &GlobalScope, + gamepad_index: u32, + supported_haptic_effects: GamepadSupportedHapticEffects, + ) -> DomRoot<GamepadHapticActuator> { + let haptic_actuator = reflect_dom_object_with_proto( + Box::new(GamepadHapticActuator::new_inherited( + gamepad_index, + supported_haptic_effects, + )), + global, + None, + ); + haptic_actuator + } +} + +impl GamepadHapticActuatorMethods for GamepadHapticActuator { + /// <https://www.w3.org/TR/gamepad/#dom-gamepadhapticactuator-effects> + fn Effects(&self, cx: JSContext) -> JSVal { + to_frozen_array(self.effects.as_slice(), cx) + } + + /// <https://www.w3.org/TR/gamepad/#dom-gamepadhapticactuator-playeffect> + fn PlayEffect( + &self, + type_: GamepadHapticEffectType, + params: &GamepadEffectParameters, + comp: InRealm, + ) -> Rc<Promise> { + let playing_effect_promise = Promise::new_in_current_realm(comp); + + // <https://www.w3.org/TR/gamepad/#dfn-valid-effect> + match type_ { + // <https://www.w3.org/TR/gamepad/#dfn-valid-dual-rumble-effect> + GamepadHapticEffectType::Dual_rumble => { + if *params.strongMagnitude < 0.0 || *params.strongMagnitude > 1.0 { + playing_effect_promise.reject_error(Error::Type( + "Strong magnitude value is not within range of 0.0 to 1.0.".to_string(), + )); + return playing_effect_promise; + } else if *params.weakMagnitude < 0.0 || *params.weakMagnitude > 1.0 { + playing_effect_promise.reject_error(Error::Type( + "Weak magnitude value is not within range of 0.0 to 1.0.".to_string(), + )); + return playing_effect_promise; + } + }, + // <https://www.w3.org/TR/gamepad/#dfn-valid-trigger-rumble-effect> + GamepadHapticEffectType::Trigger_rumble => { + if *params.strongMagnitude < 0.0 || *params.strongMagnitude > 1.0 { + playing_effect_promise.reject_error(Error::Type( + "Strong magnitude value is not within range of 0.0 to 1.0.".to_string(), + )); + return playing_effect_promise; + } else if *params.weakMagnitude < 0.0 || *params.weakMagnitude > 1.0 { + playing_effect_promise.reject_error(Error::Type( + "Weak magnitude value is not within range of 0.0 to 1.0.".to_string(), + )); + return playing_effect_promise; + } else if *params.leftTrigger < 0.0 || *params.leftTrigger > 1.0 { + playing_effect_promise.reject_error(Error::Type( + "Left trigger value is not within range of 0.0 to 1.0.".to_string(), + )); + return playing_effect_promise; + } else if *params.rightTrigger < 0.0 || *params.rightTrigger > 1.0 { + playing_effect_promise.reject_error(Error::Type( + "Right trigger value is not within range of 0.0 to 1.0.".to_string(), + )); + return playing_effect_promise; + } + }, + } + + let document = self.global().as_window().Document(); + if !document.is_fully_active() { + playing_effect_promise.reject_error(Error::InvalidState); + } + + self.sequence_id.set(self.sequence_id.get().wrapping_add(1)); + + if let Some(promise) = self.playing_effect_promise.borrow_mut().take() { + let trusted_promise = TrustedPromise::new(promise); + let _ = self.global().gamepad_task_source().queue( + task!(preempt_promise: move || { + let promise = trusted_promise.root(); + let message = DOMString::from("preempted"); + promise.resolve_native(&message); + }), + &self.global(), + ); + } + + if !self.effects.contains(&type_) { + playing_effect_promise.reject_error(Error::NotSupported); + return playing_effect_promise; + } + + *self.playing_effect_promise.borrow_mut() = Some(playing_effect_promise.clone()); + self.effect_sequence_id.set(self.sequence_id.get()); + + let context = Trusted::new(self); + let (effect_complete_sender, effect_complete_receiver) = + ipc::channel().expect("ipc channel failure"); + let (task_source, canceller) = ( + self.global().gamepad_task_source(), + self.global().task_canceller(TaskSourceName::Gamepad), + ); + let listener = HapticEffectListener { + canceller, + task_source, + context, + }; + + ROUTER.add_route( + effect_complete_receiver.to_opaque(), + Box::new(move |message| { + let msg = message.to::<bool>(); + match msg { + Ok(msg) => listener.handle_completed(msg), + Err(err) => warn!("Error receiving a GamepadMsg: {:?}", err), + } + }), + ); + + // Note: The spec says we SHOULD also pass a playEffectTimestamp for more precise playback timing + // when start_delay is non-zero, but this is left more as a footnote without much elaboration. + // <https://www.w3.org/TR/gamepad/#dfn-issue-a-haptic-effect> + + let params = DualRumbleEffectParams { + duration: params.duration as f64, + start_delay: params.startDelay as f64, + strong_magnitude: *params.strongMagnitude, + weak_magnitude: *params.weakMagnitude, + }; + let event = EmbedderMsg::PlayGamepadHapticEffect( + self.gamepad_index as usize, + embedder_traits::GamepadHapticEffectType::DualRumble(params), + effect_complete_sender, + ); + self.global().as_window().send_to_embedder(event); + + playing_effect_promise + } + + /// <https://www.w3.org/TR/gamepad/#dom-gamepadhapticactuator-reset> + fn Reset(&self, comp: InRealm) -> Rc<Promise> { + let promise = Promise::new_in_current_realm(comp); + + let document = self.global().as_window().Document(); + if !document.is_fully_active() { + promise.reject_error(Error::InvalidState); + return promise; + } + + self.sequence_id.set(self.sequence_id.get().wrapping_add(1)); + + if let Some(promise) = self.playing_effect_promise.borrow_mut().take() { + let trusted_promise = TrustedPromise::new(promise); + let _ = self.global().gamepad_task_source().queue( + task!(preempt_promise: move || { + let promise = trusted_promise.root(); + let message = DOMString::from("preempted"); + promise.resolve_native(&message); + }), + &self.global(), + ); + } + + *self.playing_effect_promise.borrow_mut() = Some(promise.clone()); + + self.reset_sequence_id.set(self.sequence_id.get()); + + let context = Trusted::new(self); + let (effect_stop_sender, effect_stop_receiver) = + ipc::channel().expect("ipc channel failure"); + let (task_source, canceller) = ( + self.global().gamepad_task_source(), + self.global().task_canceller(TaskSourceName::Gamepad), + ); + let listener = HapticEffectListener { + canceller, + task_source, + context, + }; + + ROUTER.add_route( + effect_stop_receiver.to_opaque(), + Box::new(move |message| { + let msg = message.to::<bool>(); + match msg { + Ok(msg) => listener.handle_stopped(msg), + Err(err) => warn!("Error receiving a GamepadMsg: {:?}", err), + } + }), + ); + + let event = + EmbedderMsg::StopGamepadHapticEffect(self.gamepad_index as usize, effect_stop_sender); + self.global().as_window().send_to_embedder(event); + + self.playing_effect_promise.borrow().clone().unwrap() + } +} + +impl GamepadHapticActuator { + /// <https://www.w3.org/TR/gamepad/#dom-gamepadhapticactuator-playeffect> + /// We are in the task queued by the "in-parallel" steps. + pub fn handle_haptic_effect_completed(&self, completed_successfully: bool) { + if self.effect_sequence_id.get() != self.sequence_id.get() || !completed_successfully { + return; + } + let playing_effect_promise = self.playing_effect_promise.borrow_mut().take(); + if let Some(promise) = playing_effect_promise { + let message = DOMString::from("complete"); + promise.resolve_native(&message); + } + } + + /// <https://www.w3.org/TR/gamepad/#dom-gamepadhapticactuator-reset> + /// We are in the task queued by the "in-parallel" steps. + pub fn handle_haptic_effect_stopped(&self, stopped_successfully: bool) { + if !stopped_successfully { + return; + } + + let playing_effect_promise = self.playing_effect_promise.borrow_mut().take(); + + if let Some(promise) = playing_effect_promise { + let trusted_promise = TrustedPromise::new(promise); + let sequence_id = self.sequence_id.get(); + let reset_sequence_id = self.reset_sequence_id.get(); + let _ = self.global().gamepad_task_source().queue( + task!(complete_promise: move || { + if sequence_id != reset_sequence_id { + warn!("Mismatched sequence/reset sequence ids: {} != {}", sequence_id, reset_sequence_id); + return; + } + let promise = trusted_promise.root(); + let message = DOMString::from("complete"); + promise.resolve_native(&message); + }), + &self.global(), + ); + } + } + + /// <https://www.w3.org/TR/gamepad/#handling-visibility-change> + pub fn handle_visibility_change(&self) { + if self.playing_effect_promise.borrow().is_none() { + return; + } + + let this = Trusted::new(&*self); + let _ = self.global().gamepad_task_source().queue( + task!(stop_playing_effect: move || { + let actuator = this.root(); + let Some(promise) = actuator.playing_effect_promise.borrow_mut().take() else { + return; + }; + let message = DOMString::from("preempted"); + promise.resolve_native(&message); + }), + &self.global(), + ); + + let (send, _rcv) = ipc::channel().expect("ipc channel failure"); + + let event = EmbedderMsg::StopGamepadHapticEffect(self.gamepad_index as usize, send); + self.global().as_window().send_to_embedder(event); + } +} diff --git a/components/script/dom/globalscope.rs b/components/script/dom/globalscope.rs index 0efa0af851e..acd9ac36a53 100644 --- a/components/script/dom/globalscope.rs +++ b/components/script/dom/globalscope.rs @@ -50,8 +50,9 @@ 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, GamepadEvent, GamepadUpdateType, MessagePortMsg, MsDuration, PortMessageTask, - ScriptMsg, ScriptToConstellationChan, TimerEvent, TimerEventId, TimerSchedulerMsg, TimerSource, + BroadcastMsg, GamepadEvent, GamepadSupportedHapticEffects, GamepadUpdateType, MessagePortMsg, + MsDuration, PortMessageTask, ScriptMsg, ScriptToConstellationChan, TimerEvent, TimerEventId, + TimerSchedulerMsg, TimerSource, }; use servo_url::{ImmutableOrigin, MutableOrigin, ServoUrl}; use uuid::Uuid; @@ -3140,12 +3141,13 @@ impl GlobalScope { pub fn handle_gamepad_event(&self, gamepad_event: GamepadEvent) { match gamepad_event { - GamepadEvent::Connected(index, name, bounds) => { + GamepadEvent::Connected(index, name, bounds, supported_haptic_effects) => { self.handle_gamepad_connect( index.0, name, bounds.axis_bounds, bounds.button_bounds, + supported_haptic_effects, ); }, GamepadEvent::Disconnected(index) => { @@ -3167,6 +3169,7 @@ impl GlobalScope { name: String, axis_bounds: (f64, f64), button_bounds: (f64, f64), + supported_haptic_effects: GamepadSupportedHapticEffects, ) { // TODO: 2. If document is not null and is not allowed to use the "gamepad" permission, // then abort these steps. @@ -3178,7 +3181,9 @@ impl GlobalScope { if let Some(window) = global.downcast::<Window>() { let navigator = window.Navigator(); let selected_index = navigator.select_gamepad_index(); - let gamepad = Gamepad::new(&global, selected_index, name, axis_bounds, button_bounds); + let gamepad = Gamepad::new( + &global, selected_index, name, axis_bounds, button_bounds, supported_haptic_effects + ); navigator.set_gamepad(selected_index as usize, &gamepad); } }), diff --git a/components/script/dom/mod.rs b/components/script/dom/mod.rs index 76723e8f2ce..616dd576a5c 100644 --- a/components/script/dom/mod.rs +++ b/components/script/dom/mod.rs @@ -321,6 +321,7 @@ pub mod gamepad; pub mod gamepadbutton; pub mod gamepadbuttonlist; pub mod gamepadevent; +pub mod gamepadhapticactuator; pub mod gamepadpose; pub mod globalscope; pub mod gpu; diff --git a/components/script/dom/webidls/Gamepad.webidl b/components/script/dom/webidls/Gamepad.webidl index 306aa0c216b..d716f0c0bc4 100644 --- a/components/script/dom/webidls/Gamepad.webidl +++ b/components/script/dom/webidls/Gamepad.webidl @@ -12,6 +12,7 @@ interface Gamepad { readonly attribute DOMString mapping; readonly attribute Float64Array axes; [SameObject] readonly attribute GamepadButtonList buttons; + [SameObject] readonly attribute GamepadHapticActuator vibrationActuator; }; // https://w3c.github.io/gamepad/extensions.html#partial-gamepad-interface diff --git a/components/script/dom/webidls/GamepadHapticActuator.webidl b/components/script/dom/webidls/GamepadHapticActuator.webidl new file mode 100644 index 00000000000..1961da4c541 --- /dev/null +++ b/components/script/dom/webidls/GamepadHapticActuator.webidl @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +// https://w3c.github.io/gamepad/#gamepadhapticactuator-interface +[Exposed=Window, Pref="dom.gamepad.enabled"] +interface GamepadHapticActuator { + /* [SameObject] */ readonly attribute /* FrozenArray<GamepadHapticEffectType> */ any effects; + [NewObject] + Promise<GamepadHapticsResult> playEffect( + GamepadHapticEffectType type, + optional GamepadEffectParameters params = {} + ); + [NewObject] + Promise<GamepadHapticsResult> reset(); +}; + +// https://w3c.github.io/gamepad/#gamepadhapticsresult-enum +enum GamepadHapticsResult { + "complete", + "preempted" +}; + +// https://w3c.github.io/gamepad/#dom-gamepadhapticeffecttype +enum GamepadHapticEffectType { + "dual-rumble", + "trigger-rumble" +}; + +// https://w3c.github.io/gamepad/#dom-gamepadeffectparameters +dictionary GamepadEffectParameters { + unsigned long long duration = 0; + unsigned long long startDelay = 0; + double strongMagnitude = 0.0; + double weakMagnitude = 0.0; + double leftTrigger = 0.0; + double rightTrigger = 0.0; +}; diff --git a/components/shared/embedder/lib.rs b/components/shared/embedder/lib.rs index 0a5cfdbb479..67bf2468814 100644 --- a/components/shared/embedder/lib.rs +++ b/components/shared/embedder/lib.rs @@ -214,6 +214,10 @@ pub enum EmbedderMsg { ReadyToPresent(Vec<WebViewId>), /// The given event was delivered to a pipeline in the given browser. EventDelivered(CompositorEventVariant), + /// Request to play a haptic effect on a connected gamepad. + PlayGamepadHapticEffect(usize, GamepadHapticEffectType, IpcSender<bool>), + /// Request to stop a haptic effect on a connected gamepad. + StopGamepadHapticEffect(usize, IpcSender<bool>), } /// The variant of CompositorEvent that was delivered to a pipeline. @@ -268,6 +272,8 @@ impl Debug for EmbedderMsg { EmbedderMsg::ShowContextMenu(..) => write!(f, "ShowContextMenu"), EmbedderMsg::ReadyToPresent(..) => write!(f, "ReadyToPresent"), EmbedderMsg::EventDelivered(..) => write!(f, "HitTestedEvent"), + EmbedderMsg::PlayGamepadHapticEffect(..) => write!(f, "PlayGamepadHapticEffect"), + EmbedderMsg::StopGamepadHapticEffect(..) => write!(f, "StopGamepadHapticEffect"), } } } @@ -388,3 +394,18 @@ pub enum InputMethodType { Url, Week, } + +#[derive(Clone, Debug, Deserialize, Serialize)] +/// <https://w3.org/TR/gamepad/#dom-gamepadhapticeffecttype-dual-rumble> +pub struct DualRumbleEffectParams { + pub duration: f64, + pub start_delay: f64, + pub strong_magnitude: f64, + pub weak_magnitude: f64, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +/// <https://w3.org/TR/gamepad/#dom-gamepadhapticeffecttype> +pub enum GamepadHapticEffectType { + DualRumble(DualRumbleEffectParams), +} diff --git a/components/shared/script/lib.rs b/components/shared/script/lib.rs index ff6e2baeefb..895923930cd 100644 --- a/components/shared/script/lib.rs +++ b/components/shared/script/lib.rs @@ -1093,11 +1093,25 @@ pub struct GamepadInputBounds { } #[derive(Clone, Debug, Deserialize, Serialize)] +/// The haptic effects supported by this gamepad +pub struct GamepadSupportedHapticEffects { + /// Gamepad support for dual rumble effects + pub supports_dual_rumble: bool, + /// Gamepad support for trigger rumble effects + pub supports_trigger_rumble: bool, +} + +#[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), + Connected( + GamepadIndex, + String, + GamepadInputBounds, + GamepadSupportedHapticEffects, + ), /// An existing gamepad has been disconnected /// <https://www.w3.org/TR/gamepad/#event-gamepaddisconnected> Disconnected(GamepadIndex), diff --git a/ports/servoshell/Cargo.toml b/ports/servoshell/Cargo.toml index be995af59a1..590b6f07f99 100644 --- a/ports/servoshell/Cargo.toml +++ b/ports/servoshell/Cargo.toml @@ -98,7 +98,7 @@ egui = { version = "0.28.1" } egui_glow = { version = "0.28.1", features = ["winit"] } egui-winit = { version = "0.28.1", default-features = false, features = ["clipboard", "wayland"] } euclid = { workspace = true } -gilrs = "0.10.8" +gilrs = { git = "https://gitlab.com/gilrs-project/gilrs", rev = "eafb7f2ef488874188c5d75adce9aef486be9d4e" } gleam = { workspace = true } glow = "0.13.1" keyboard-types = { workspace = true } diff --git a/ports/servoshell/desktop/tracing.rs b/ports/servoshell/desktop/tracing.rs index 59c07105c94..2edddf645cc 100644 --- a/ports/servoshell/desktop/tracing.rs +++ b/ports/servoshell/desktop/tracing.rs @@ -176,6 +176,8 @@ mod from_servo { Self::OnDevtoolsStarted(..) => target!("OnDevtoolsStarted"), Self::ReadyToPresent(..) => target!("ReadyToPresent"), Self::EventDelivered(..) => target!("EventDelivered"), + Self::PlayGamepadHapticEffect(..) => target!("PlayGamepadHapticEffect"), + Self::StopGamepadHapticEffect(..) => target!("StopGamepadHapticEffect"), } } } diff --git a/ports/servoshell/desktop/webview.rs b/ports/servoshell/desktop/webview.rs index 920a6a61477..558cf52e95e 100644 --- a/ports/servoshell/desktop/webview.rs +++ b/ports/servoshell/desktop/webview.rs @@ -12,18 +12,21 @@ use std::{env, thread}; use arboard::Clipboard; use euclid::{Point2D, Vector2D}; +use gilrs::ff::{BaseEffect, BaseEffectType, Effect, EffectBuilder, Repeat, Replay, Ticks}; use gilrs::{EventType, Gilrs}; use keyboard_types::{Key, KeyboardEvent, Modifiers, ShortcutMatcher}; use log::{debug, error, info, trace, warn}; use servo::base::id::TopLevelBrowsingContextId as WebViewId; use servo::compositing::windowing::{EmbedderEvent, WebRenderDebugOption}; use servo::embedder_traits::{ - CompositorEventVariant, ContextMenuResult, EmbedderMsg, FilterPattern, PermissionPrompt, - PermissionRequest, PromptDefinition, PromptOrigin, PromptResult, + CompositorEventVariant, ContextMenuResult, DualRumbleEffectParams, EmbedderMsg, FilterPattern, + GamepadHapticEffectType, PermissionPrompt, PermissionRequest, PromptDefinition, PromptOrigin, + PromptResult, }; +use servo::ipc_channel::ipc::IpcSender; use servo::script_traits::{ - GamepadEvent, GamepadIndex, GamepadInputBounds, GamepadUpdateType, TouchEventType, - TraversalDirection, + GamepadEvent, GamepadIndex, GamepadInputBounds, GamepadSupportedHapticEffects, + GamepadUpdateType, TouchEventType, TraversalDirection, }; use servo::servo_config::opts; use servo::servo_url::ServoUrl; @@ -59,6 +62,7 @@ pub struct WebViewManager<Window: WindowPortsMethods + ?Sized> { event_queue: Vec<EmbedderEvent>, clipboard: Option<Clipboard>, gamepad: Option<Gilrs>, + haptic_effects: HashMap<usize, HapticEffect>, shutdown_requested: bool, load_status: LoadStatus, } @@ -80,6 +84,11 @@ pub enum LoadStatus { LoadComplete, } +pub struct HapticEffect { + pub effect: Effect, + pub sender: IpcSender<bool>, +} + impl<Window> WebViewManager<Window> where Window: WindowPortsMethods + ?Sized, @@ -108,6 +117,8 @@ where None }, }, + haptic_effects: HashMap::default(), + event_queue: Vec::new(), shutdown_requested: false, load_status: LoadStatus::LoadComplete, @@ -218,11 +229,32 @@ where axis_bounds: (-1.0, 1.0), button_bounds: (0.0, 1.0), }; - gamepad_event = Some(GamepadEvent::Connected(index, name, bounds)); + // GilRs does not yet support trigger rumble + let supported_haptic_effects = GamepadSupportedHapticEffects { + supports_dual_rumble: true, + supports_trigger_rumble: false, + }; + gamepad_event = Some(GamepadEvent::Connected( + index, + name, + bounds, + supported_haptic_effects, + )); }, EventType::Disconnected => { gamepad_event = Some(GamepadEvent::Disconnected(index)); }, + EventType::ForceFeedbackEffectCompleted => { + let Some(effect) = self.haptic_effects.get(&event.id.into()) else { + warn!("Failed to find haptic effect for id {}", event.id); + return; + }; + effect + .sender + .send(true) + .expect("Failed to send haptic effect completion."); + self.haptic_effects.remove(&event.id.into()); + }, _ => {}, } @@ -258,6 +290,79 @@ where } } + fn play_haptic_effect( + &mut self, + index: usize, + params: DualRumbleEffectParams, + effect_complete_sender: IpcSender<bool>, + ) { + let Some(ref mut gilrs) = self.gamepad else { + debug!("Unable to get gilrs instance!"); + return; + }; + + if let Some(connected_gamepad) = gilrs + .gamepads() + .find(|gamepad| usize::from(gamepad.0) == index) + { + let start_delay = Ticks::from_ms(params.start_delay as u32); + let duration = Ticks::from_ms(params.duration as u32); + let strong_magnitude = (params.strong_magnitude * u16::MAX as f64).round() as u16; + let weak_magnitude = (params.weak_magnitude * u16::MAX as f64).round() as u16; + + let scheduling = Replay { + after: start_delay, + play_for: duration, + with_delay: Ticks::from_ms(0), + }; + let effect = EffectBuilder::new() + .add_effect(BaseEffect { + kind: BaseEffectType::Strong { magnitude: strong_magnitude }, + scheduling, + envelope: Default::default(), + }) + .add_effect(BaseEffect { + kind: BaseEffectType::Weak { magnitude: weak_magnitude }, + scheduling, + envelope: Default::default(), + }) + .repeat(Repeat::For(start_delay + duration)) + .add_gamepad(&connected_gamepad.1) + .finish(gilrs) + .expect("Failed to create haptic effect, ensure connected gamepad supports force feedback."); + self.haptic_effects.insert( + index, + HapticEffect { + effect, + sender: effect_complete_sender, + }, + ); + self.haptic_effects[&index] + .effect + .play() + .expect("Failed to play haptic effect."); + } else { + debug!("Couldn't find connected gamepad to play haptic effect on"); + } + } + + fn stop_haptic_effect(&mut self, index: usize) -> bool { + let Some(haptic_effect) = self.haptic_effects.get(&index) else { + return false; + }; + + let stopped_successfully = match haptic_effect.effect.stop() { + Ok(()) => true, + Err(e) => { + debug!("Failed to stop haptic effect: {:?}", e); + false + }, + }; + self.haptic_effects.remove(&index); + + stopped_successfully + } + pub fn shutdown_requested(&self) -> bool { self.shutdown_requested } @@ -744,6 +849,19 @@ where .push(EmbedderEvent::FocusWebView(webview_id)); } }, + EmbedderMsg::PlayGamepadHapticEffect(index, effect, effect_complete_sender) => { + match effect { + GamepadHapticEffectType::DualRumble(params) => { + self.play_haptic_effect(index, params, effect_complete_sender); + }, + } + }, + EmbedderMsg::StopGamepadHapticEffect(index, haptic_stop_sender) => { + let stopped_successfully = self.stop_haptic_effect(index); + haptic_stop_sender + .send(stopped_successfully) + .expect("Failed to send haptic stop result"); + }, } } diff --git a/ports/servoshell/egl/servo_glue.rs b/ports/servoshell/egl/servo_glue.rs index 882451a4196..13022145fe5 100644 --- a/ports/servoshell/egl/servo_glue.rs +++ b/ports/servoshell/egl/servo_glue.rs @@ -623,7 +623,9 @@ impl ServoGlue { EmbedderMsg::HeadParsed | EmbedderMsg::SetFullscreenState(..) | EmbedderMsg::ReportProfile(..) | - EmbedderMsg::EventDelivered(..) => {}, + EmbedderMsg::EventDelivered(..) | + EmbedderMsg::PlayGamepadHapticEffect(..) | + EmbedderMsg::StopGamepadHapticEffect(..) => {}, } } diff --git a/tests/wpt/meta-legacy-layout/gamepad/idlharness.window.js.ini b/tests/wpt/meta-legacy-layout/gamepad/idlharness.window.js.ini index 37c2bd87ea1..577d82d9237 100644 --- a/tests/wpt/meta-legacy-layout/gamepad/idlharness.window.js.ini +++ b/tests/wpt/meta-legacy-layout/gamepad/idlharness.window.js.ini @@ -1,28 +1,4 @@ [idlharness.window.html] - [Gamepad interface: attribute vibrationActuator] - expected: FAIL - - [GamepadHapticActuator interface: existence and properties of interface object] - expected: FAIL - - [GamepadHapticActuator interface object length] - expected: FAIL - - [GamepadHapticActuator interface object name] - expected: FAIL - - [GamepadHapticActuator interface: existence and properties of interface prototype object] - expected: FAIL - - [GamepadHapticActuator interface: existence and properties of interface prototype object's "constructor" property] - expected: FAIL - - [GamepadHapticActuator interface: existence and properties of interface prototype object's @@unscopables property] - expected: FAIL - - [GamepadHapticActuator interface: attribute effects] - expected: FAIL - [GamepadHapticActuator interface: operation playEffect(GamepadHapticEffectType, optional GamepadEffectParameters)] expected: FAIL diff --git a/tests/wpt/meta/gamepad/idlharness.window.js.ini b/tests/wpt/meta/gamepad/idlharness.window.js.ini index 37c2bd87ea1..577d82d9237 100644 --- a/tests/wpt/meta/gamepad/idlharness.window.js.ini +++ b/tests/wpt/meta/gamepad/idlharness.window.js.ini @@ -1,28 +1,4 @@ [idlharness.window.html] - [Gamepad interface: attribute vibrationActuator] - expected: FAIL - - [GamepadHapticActuator interface: existence and properties of interface object] - expected: FAIL - - [GamepadHapticActuator interface object length] - expected: FAIL - - [GamepadHapticActuator interface object name] - expected: FAIL - - [GamepadHapticActuator interface: existence and properties of interface prototype object] - expected: FAIL - - [GamepadHapticActuator interface: existence and properties of interface prototype object's "constructor" property] - expected: FAIL - - [GamepadHapticActuator interface: existence and properties of interface prototype object's @@unscopables property] - expected: FAIL - - [GamepadHapticActuator interface: attribute effects] - expected: FAIL - [GamepadHapticActuator interface: operation playEffect(GamepadHapticEffectType, optional GamepadEffectParameters)] expected: FAIL |