Gamepad: Implement GamepadHapticActuator (#32046)

* Implement Servo side of GamepadHapticActuator

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Get build working

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Create effect handling on embedder side

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Update tracing for GamepadHapticEffect

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Update gilrs to point to commit with effect complete event

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Implement playing and preempting haptic effects

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Update IDL to add trigger rumble

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Update WPT expectations

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Handle stopping haptic effects from reset()

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* ./mach fmt, fix test-tidy issues

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Add extra validity checks for trigger rumble

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Retrieve supported haptic effects from embedder

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Fix test expectations

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Add missing spec link, pin gilrs commit

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* servoshell cargo formatting

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Fix Cargo.toml

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Additional comments, realm proof, naming

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* ./mach fmt

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Update gilrs rev to gilrs-core 0.5.12 release

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Implement sequence ids for gamepad haptic promises

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Take playing effect promise instead of cloning

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Implement listener for reset function

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Fix Cargo.lock

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Restructure IPC listeners, add comments, handle visibility change

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Check that haptic effect still exists before handling ff completion event

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Visibility steps, add InRealm bindings for promises

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Add Gamepad EmbedderMsg arms to egl servo_glue

Signed-off-by: Daniel Adams <msub2official@gmail.com>

---------

Signed-off-by: Daniel Adams <msub2official@gmail.com>
This commit is contained in:
Daniel Adams 2024-07-19 20:29:27 -10:00 committed by GitHub
parent 9212ed203a
commit 2c17de7fa7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 652 additions and 68 deletions

10
Cargo.lock generated
View file

@ -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",

View file

@ -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"),
}
}
}

View file

@ -176,6 +176,10 @@ DOMInterfaces = {
'CreateRenderPipelineAsync',
'CreateShaderModule' # Creates promise for compilation info
],
},
'GamepadHapticActuator': {
'inRealms': ['PlayEffect', 'Reset']
}
}

View file

@ -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"));

View file

@ -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>

View file

@ -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);
}
}

View file

@ -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);
}
}),

View file

@ -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;

View file

@ -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

View file

@ -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;
};

View file

@ -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),
}

View file

@ -1092,12 +1092,26 @@ 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
/// <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),

View file

@ -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 }

View file

@ -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"),
}
}
}

View file

@ -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");
},
}
}

View file

@ -623,7 +623,9 @@ impl ServoGlue {
EmbedderMsg::HeadParsed |
EmbedderMsg::SetFullscreenState(..) |
EmbedderMsg::ReportProfile(..) |
EmbedderMsg::EventDelivered(..) => {},
EmbedderMsg::EventDelivered(..) |
EmbedderMsg::PlayGamepadHapticEffect(..) |
EmbedderMsg::StopGamepadHapticEffect(..) => {},
}
}

View file

@ -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

View file

@ -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