diff --git a/Cargo.lock b/Cargo.lock index 4216c66393b9..0ecc4142d605 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1994,8 +1994,7 @@ dependencies = [ [[package]] name = "gilrs" version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "499067aa54af19f88732dc418f61f23d5912de1518665bb0eca034ca0d07574c" +source = "git+https://gitlab.com/gilrs-project/gilrs?rev=09576ad0fb29db4a9238d34b86e7a5eaf628ee95#09576ad0fb29db4a9238d34b86e7a5eaf628ee95" dependencies = [ "fnv", "gilrs-core", @@ -2007,8 +2006,7 @@ dependencies = [ [[package]] name = "gilrs-core" version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85c132270a155f2548e67d66e731075c336c39098afc555752f3df8f882c720e" +source = "git+https://gitlab.com/gilrs-project/gilrs?rev=09576ad0fb29db4a9238d34b86e7a5eaf628ee95#09576ad0fb29db4a9238d34b86e7a5eaf628ee95" dependencies = [ "core-foundation", "inotify", diff --git a/components/constellation/tracing.rs b/components/constellation/tracing.rs index 040a8bd32696..f1528cd7a1b4 100644 --- a/components/constellation/tracing.rs +++ b/components/constellation/tracing.rs @@ -240,6 +240,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/gamepad.rs b/components/script/dom/gamepad.rs index cee1077d00fc..951be3d05d53 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, + vibration_actuator: Dom, } 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 { - 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 { 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 { + 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 + } } /// diff --git a/components/script/dom/gamepadhapticactuator.rs b/components/script/dom/gamepadhapticactuator.rs new file mode 100644 index 000000000000..08ccb156bf57 --- /dev/null +++ b/components/script/dom/gamepadhapticactuator.rs @@ -0,0 +1,278 @@ +/* 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::rc::Rc; + +use dom_struct::dom_struct; +use embedder_traits::{DualRumbleEffectParams, EmbedderMsg}; +use js::jsval::JSVal; +use script_traits::GamepadSupportedHapticEffects; + +use crate::dom::bindings::cell::DomRefCell; +use crate::dom::bindings::codegen::Bindings::GamepadHapticActuatorBinding::{ + GamepadEffectParameters, GamepadHapticActuatorMethods, GamepadHapticEffectType, +}; +use crate::dom::bindings::codegen::Bindings::PerformanceBinding::Performance_Binding::PerformanceMethods; +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::script_runtime::JSContext; +use crate::task_source::TaskSource; + +#[dom_struct] +pub struct GamepadHapticActuator { + reflector_: Reflector, + gamepad_index: u32, + effects: Vec, + #[ignore_malloc_size_of = "promises are hard"] + playing_effect_promise: DomRefCell>>, + #[ignore_malloc_size_of = "promises are hard"] + reset_result_promise: DomRefCell>>, +} + +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), + reset_result_promise: DomRefCell::new(None), + } + } + + pub fn new( + global: &GlobalScope, + gamepad_index: u32, + supported_haptic_effects: GamepadSupportedHapticEffects, + ) -> DomRoot { + Self::new_with_proto(global, gamepad_index, supported_haptic_effects) + } + + fn new_with_proto( + global: &GlobalScope, + gamepad_index: u32, + supported_haptic_effects: GamepadSupportedHapticEffects, + ) -> DomRoot { + 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 { + /// + fn Effects(&self, cx: JSContext) -> JSVal { + to_frozen_array(self.effects.as_slice(), cx) + } + + /// + fn PlayEffect( + &self, + type_: GamepadHapticEffectType, + params: &GamepadEffectParameters, + ) -> Rc { + let playing_effect_promise = Promise::new(&self.global()); + + // + match type_ { + // + 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(), + )); + } 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(), + )); + } + }, + 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(), + )); + } 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(), + )); + } 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(), + )); + } 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(), + )); + } + }, + } + + let document = self.global().as_window().Document(); + if !document.is_fully_active() { + playing_effect_promise.reject_error(Error::InvalidState); + } + + if self.playing_effect_promise.borrow().is_some() { + let trusted_promise = TrustedPromise::new( + self.playing_effect_promise + .borrow() + .clone() + .expect("Promise is null!"), + ); + *self.playing_effect_promise.borrow_mut() = None; + 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); + } + + *self.playing_effect_promise.borrow_mut() = Some(playing_effect_promise.clone()); + let play_effect_timestamp = self.global().performance().Now(); + + 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), + ); + self.global().as_window().send_to_embedder(event); + + playing_effect_promise + } + + /// + fn Reset(&self) -> Rc { + let reset_result_promise = Promise::new(&self.global()); + + let document = self.global().as_window().Document(); + if !document.is_fully_active() { + reset_result_promise.reject_error(Error::InvalidState); + } + + if self.playing_effect_promise.borrow().is_some() { + *self.reset_result_promise.borrow_mut() = Some(reset_result_promise.clone()); + + let event = EmbedderMsg::StopGamepadHapticEffect(self.gamepad_index as usize); + self.global().as_window().send_to_embedder(event); + } else { + let message = DOMString::from("complete"); + reset_result_promise.resolve_native(&message); + } + + reset_result_promise + } +} + +impl GamepadHapticActuator { + pub fn has_playing_effect_promise(&self) -> bool { + self.playing_effect_promise.borrow().is_some() + } + + pub fn resolve_playing_effect_promise(&self) { + let playing_effect_promise = self.playing_effect_promise.borrow().clone(); + if let Some(promise) = playing_effect_promise { + let message = DOMString::from("complete"); + promise.resolve_native(&message); + } + } + + pub fn has_reset_result_promise(&self) -> bool { + self.reset_result_promise.borrow().is_some() + } + + pub fn resolve_reset_result_promise(&self) { + let playing_effect_promise = self.playing_effect_promise.borrow().clone(); + let reset_result_promise = self.reset_result_promise.borrow().clone(); + + if playing_effect_promise.is_some() { + let trusted_promise = TrustedPromise::new( + self.playing_effect_promise + .borrow() + .clone() + .expect("Promise is null!"), + ); + 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() = None; + } + + if let Some(promise) = reset_result_promise { + let message = DOMString::from("complete"); + promise.resolve_native(&message); + } + } + + /// + #[allow(dead_code)] + pub fn handle_visibility_change(&self) { + if self.playing_effect_promise.borrow().is_none() { + return; + } + + let trusted_promise = TrustedPromise::new( + self.playing_effect_promise + .borrow() + .clone() + .expect("Promise is null!"), + ); + + let this = Trusted::new(&*self); + + let _ = self.global().gamepad_task_source().queue( + task!(stop_playing_effect: move || { + let promise = trusted_promise.root(); + let actuator = this.root(); + let message = DOMString::from("preempted"); + promise.resolve_native(&message); + *actuator.playing_effect_promise.borrow_mut() = None; + }), + &self.global(), + ); + + let event = EmbedderMsg::StopGamepadHapticEffect(self.gamepad_index as usize); + self.global().as_window().send_to_embedder(event); + } +} diff --git a/components/script/dom/globalscope.rs b/components/script/dom/globalscope.rs index 11edae2a21de..8c050965931c 100644 --- a/components/script/dom/globalscope.rs +++ b/components/script/dom/globalscope.rs @@ -51,8 +51,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; @@ -3113,12 +3114,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) => { @@ -3127,6 +3129,12 @@ impl GlobalScope { GamepadEvent::Updated(index, update_type) => { self.receive_new_gamepad_button_or_axis(index.0, update_type); }, + GamepadEvent::HapticEffectCompleted(index) => { + self.handle_gamepad_haptic_effect_completed(index.0); + }, + GamepadEvent::HapticEffectStopped(index) => { + self.handle_gamepad_haptic_effect_stopped(index.0); + }, }; } @@ -3140,6 +3148,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. @@ -3151,7 +3160,9 @@ impl GlobalScope { if let Some(window) = global.downcast::() { 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); } }), @@ -3234,6 +3245,44 @@ impl GlobalScope { .expect("Failed to queue update gamepad state task."); } + pub fn handle_gamepad_haptic_effect_completed(&self, index: usize) { + let this = Trusted::new(&*self); + self.gamepad_task_source() + .queue_with_canceller( + task!(gamepad_haptic_effect_completed: move || { + let global = this.root(); + if let Some(window) = global.downcast::() { + if let Some(gamepad) = window.Navigator().get_gamepad(index) { + if gamepad.vibration_actuator().has_playing_effect_promise() { + gamepad.vibration_actuator().resolve_playing_effect_promise(); + } + } + } + }), + &self.task_canceller(TaskSourceName::Gamepad), + ) + .expect("Failed to queue gamepad haptic effect completed task."); + } + + pub fn handle_gamepad_haptic_effect_stopped(&self, index: usize) { + let this = Trusted::new(&*self); + self.gamepad_task_source() + .queue_with_canceller( + task!(gamepad_haptic_effect_stopped: move || { + let global = this.root(); + if let Some(window) = global.downcast::() { + if let Some(gamepad) = window.Navigator().get_gamepad(index) { + if gamepad.vibration_actuator().has_reset_result_promise() { + gamepad.vibration_actuator().resolve_reset_result_promise(); + } + } + } + }), + &self.task_canceller(TaskSourceName::Gamepad), + ) + .expect("Failed to queue gamepad haptic effect stopped task."); + } + pub(crate) fn current_group_label(&self) -> Option { self.console_group_stack .borrow() diff --git a/components/script/dom/mod.rs b/components/script/dom/mod.rs index 590a90f93702..703f6a2a8fc7 100644 --- a/components/script/dom/mod.rs +++ b/components/script/dom/mod.rs @@ -319,6 +319,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 306aa0c216b3..d716f0c0bc4c 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 000000000000..a1277e16fecb --- /dev/null +++ b/components/script/dom/webidls/GamepadHapticActuator.webidl @@ -0,0 +1,36 @@ +/* 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 */ any effects; + Promise playEffect( + GamepadHapticEffectType type, + optional GamepadEffectParameters params = {} + ); + Promise 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 0acf7d01ce5a..db3740c9ceb9 100644 --- a/components/shared/embedder/lib.rs +++ b/components/shared/embedder/lib.rs @@ -214,6 +214,10 @@ pub enum EmbedderMsg { ReadyToPresent(Vec), /// 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), + /// Request to stop a haptic effect on a connected gamepad. + StopGamepadHapticEffect(usize), } /// 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"), } } } @@ -368,3 +374,16 @@ pub enum PermissionRequest { Granted, Denied, } + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct DualRumbleEffectParams { + pub duration: f64, + pub start_delay: f64, + pub strong_magnitude: f64, + pub weak_magnitude: f64, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub enum GamepadHapticEffectType { + DualRumble(DualRumbleEffectParams), +} diff --git a/components/shared/script/lib.rs b/components/shared/script/lib.rs index 76d0e74bd311..ebcd8736dabe 100644 --- a/components/shared/script/lib.rs +++ b/components/shared/script/lib.rs @@ -1340,18 +1340,36 @@ pub struct GamepadInputBounds { pub button_bounds: (f64, f64), } +#[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 /// - Connected(GamepadIndex, String, GamepadInputBounds), + Connected( + GamepadIndex, + String, + GamepadInputBounds, + GamepadSupportedHapticEffects, + ), /// An existing gamepad has been disconnected /// Disconnected(GamepadIndex), /// An existing gamepad has been updated /// Updated(GamepadIndex, GamepadUpdateType), + /// A gamepad haptic effect has completed + HapticEffectCompleted(GamepadIndex), + /// A gamepad haptic effect has been stopped + HapticEffectStopped(GamepadIndex), } #[derive(Clone, Debug, Deserialize, Serialize)] diff --git a/ports/servoshell/Cargo.toml b/ports/servoshell/Cargo.toml index 9e916dd68a99..9309444c951b 100644 --- a/ports/servoshell/Cargo.toml +++ b/ports/servoshell/Cargo.toml @@ -62,7 +62,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.6" +gilrs = { git = "https://gitlab.com/gilrs-project/gilrs", rev = "09576ad0fb29db4a9238d34b86e7a5eaf628ee95" } gleam = { workspace = true } glow = "0.12.2" keyboard-types = { workspace = true } diff --git a/ports/servoshell/tracing.rs b/ports/servoshell/tracing.rs index 394765d14236..83bdcb2ce6d9 100644 --- a/ports/servoshell/tracing.rs +++ b/ports/servoshell/tracing.rs @@ -174,6 +174,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/webview.rs b/ports/servoshell/webview.rs index 34d87da6fe32..d9bd2afacada 100644 --- a/ports/servoshell/webview.rs +++ b/ports/servoshell/webview.rs @@ -12,17 +12,20 @@ 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::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::msg::constellation_msg::{TopLevelBrowsingContextId as WebViewId, TraversalDirection}; use servo::script_traits::{ - GamepadEvent, GamepadIndex, GamepadInputBounds, GamepadUpdateType, TouchEventType, + GamepadEvent, GamepadIndex, GamepadInputBounds, GamepadSupportedHapticEffects, + GamepadUpdateType, TouchEventType, }; use servo::servo_config::opts; use servo::servo_url::ServoUrl; @@ -56,6 +59,7 @@ pub struct WebViewManager { event_queue: Vec, clipboard: Option, gamepad: Option, + haptic_effects: Vec>, shutdown_requested: bool, load_status: LoadStatus, } @@ -104,6 +108,7 @@ where None }, }, + haptic_effects: vec![], event_queue: Vec::new(), shutdown_requested: false, load_status: LoadStatus::LoadComplete, @@ -205,16 +210,32 @@ where } }, EventType::Connected => { + let len = self.haptic_effects.len(); + self.haptic_effects.resize_with(len + 1, Default::default); + let name = String::from(name); let bounds = GamepadInputBounds { 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 => { + gamepad_event = Some(GamepadEvent::HapticEffectCompleted(index)); + }, _ => {}, } @@ -250,6 +271,65 @@ where } } + fn play_haptic_effect(&mut self, index: usize, params: DualRumbleEffectParams) { + if let Some(ref mut gilrs) = self.gamepad { + 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.as_mut_slice()[index] = Some(effect); + self.haptic_effects[index] + .as_ref() + .expect("No haptic effect found") + .play() + .expect("Failed to play haptic effect."); + } else { + println!("Couldn't find connected gamepad to play haptic effect on"); + } + } + } + + fn stop_haptic_effect(&mut self, index: usize) { + let Some(ref effect) = self.haptic_effects[index] else { + return; + }; + + effect.stop().expect("Failed to stop haptic effect."); + self.haptic_effects.as_mut_slice()[index] = None; + + if self.haptic_effects.iter().all(Option::is_none) { + self.haptic_effects.clear(); + } + + let event = GamepadEvent::HapticEffectStopped(GamepadIndex(index)); + self.event_queue.push(EmbedderEvent::Gamepad(event)); + } + pub fn shutdown_requested(&self) -> bool { self.shutdown_requested } @@ -735,6 +815,12 @@ where .push(EmbedderEvent::FocusWebView(webview_id)); } }, + EmbedderMsg::PlayGamepadHapticEffect(index, effect) => match effect { + GamepadHapticEffectType::DualRumble(params) => { + self.play_haptic_effect(index, params) + }, + }, + EmbedderMsg::StopGamepadHapticEffect(index) => self.stop_haptic_effect(index), } } 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 37c2bd87ea1f..577d82d9237b 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 37c2bd87ea1f..577d82d9237b 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