aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock10
-rw-r--r--components/constellation/tracing.rs2
-rw-r--r--components/script/dom/bindings/codegen/Bindings.conf4
-rw-r--r--components/script/dom/document.rs13
-rw-r--r--components/script/dom/gamepad.rs29
-rw-r--r--components/script/dom/gamepadhapticactuator.rs388
-rw-r--r--components/script/dom/globalscope.rs13
-rw-r--r--components/script/dom/mod.rs1
-rw-r--r--components/script/dom/webidls/Gamepad.webidl1
-rw-r--r--components/script/dom/webidls/GamepadHapticActuator.webidl38
-rw-r--r--components/shared/embedder/lib.rs21
-rw-r--r--components/shared/script/lib.rs16
-rw-r--r--ports/servoshell/Cargo.toml2
-rw-r--r--ports/servoshell/desktop/tracing.rs2
-rw-r--r--ports/servoshell/desktop/webview.rs128
-rw-r--r--ports/servoshell/egl/servo_glue.rs4
-rw-r--r--tests/wpt/meta-legacy-layout/gamepad/idlharness.window.js.ini24
-rw-r--r--tests/wpt/meta/gamepad/idlharness.window.js.ini24
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