mirror of
https://github.com/servo/servo.git
synced 2025-07-24 15:50:21 +01:00
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:
parent
9212ed203a
commit
2c17de7fa7
18 changed files with 652 additions and 68 deletions
10
Cargo.lock
generated
10
Cargo.lock
generated
|
@ -2130,9 +2130,8 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gilrs"
|
name = "gilrs"
|
||||||
version = "0.10.8"
|
version = "0.10.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://gitlab.com/gilrs-project/gilrs?rev=eafb7f2ef488874188c5d75adce9aef486be9d4e#eafb7f2ef488874188c5d75adce9aef486be9d4e"
|
||||||
checksum = "3f226b8f4d9bc7da93de8efd8747c6b1086409ca3f4b6d51e9a7f5461a9183fe"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fnv",
|
"fnv",
|
||||||
"gilrs-core",
|
"gilrs-core",
|
||||||
|
@ -2143,9 +2142,8 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gilrs-core"
|
name = "gilrs-core"
|
||||||
version = "0.5.13"
|
version = "0.5.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://gitlab.com/gilrs-project/gilrs?rev=eafb7f2ef488874188c5d75adce9aef486be9d4e#eafb7f2ef488874188c5d75adce9aef486be9d4e"
|
||||||
checksum = "bbb5e8d912059b33b463831c16b838d15c4772d584ce332e4a80f6dffdae2bc1"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"core-foundation",
|
"core-foundation",
|
||||||
"inotify",
|
"inotify",
|
||||||
|
|
|
@ -237,6 +237,8 @@ mod from_script {
|
||||||
Self::OnDevtoolsStarted(..) => target_variant!("OnDevtoolsStarted"),
|
Self::OnDevtoolsStarted(..) => target_variant!("OnDevtoolsStarted"),
|
||||||
Self::ReadyToPresent(..) => target_variant!("ReadyToPresent"),
|
Self::ReadyToPresent(..) => target_variant!("ReadyToPresent"),
|
||||||
Self::EventDelivered(..) => target_variant!("EventDelivered"),
|
Self::EventDelivered(..) => target_variant!("EventDelivered"),
|
||||||
|
Self::PlayGamepadHapticEffect(..) => target_variant!("PlayGamepadHapticEffect"),
|
||||||
|
Self::StopGamepadHapticEffect(..) => target_variant!("StopGamepadHapticEffect"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -176,6 +176,10 @@ DOMInterfaces = {
|
||||||
'CreateRenderPipelineAsync',
|
'CreateRenderPipelineAsync',
|
||||||
'CreateShaderModule' # Creates promise for compilation info
|
'CreateShaderModule' # Creates promise for compilation info
|
||||||
],
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
'GamepadHapticActuator': {
|
||||||
|
'inRealms': ['PlayEffect', 'Reset']
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4125,6 +4125,19 @@ impl Document {
|
||||||
// Step 6 Run any page visibility change steps which may be defined in other specifications, with visibility
|
// 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.
|
// 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.
|
// Step 7 Fire an event named visibilitychange at document, with its bubbles attribute initialized to true.
|
||||||
self.upcast::<EventTarget>()
|
self.upcast::<EventTarget>()
|
||||||
.fire_bubbling_event(atom!("visibilitychange"));
|
.fire_bubbling_event(atom!("visibilitychange"));
|
||||||
|
|
|
@ -6,7 +6,7 @@ use std::cell::Cell;
|
||||||
|
|
||||||
use dom_struct::dom_struct;
|
use dom_struct::dom_struct;
|
||||||
use js::typedarray::{Float64, Float64Array};
|
use js::typedarray::{Float64, Float64Array};
|
||||||
use script_traits::GamepadUpdateType;
|
use script_traits::{GamepadSupportedHapticEffects, GamepadUpdateType};
|
||||||
|
|
||||||
use super::bindings::buffer_source::HeapBufferSource;
|
use super::bindings::buffer_source::HeapBufferSource;
|
||||||
use crate::dom::bindings::codegen::Bindings::GamepadBinding::{GamepadHand, GamepadMethods};
|
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::eventtarget::EventTarget;
|
||||||
use crate::dom::gamepadbuttonlist::GamepadButtonList;
|
use crate::dom::gamepadbuttonlist::GamepadButtonList;
|
||||||
use crate::dom::gamepadevent::{GamepadEvent, GamepadEventType};
|
use crate::dom::gamepadevent::{GamepadEvent, GamepadEventType};
|
||||||
|
use crate::dom::gamepadhapticactuator::GamepadHapticActuator;
|
||||||
use crate::dom::gamepadpose::GamepadPose;
|
use crate::dom::gamepadpose::GamepadPose;
|
||||||
use crate::dom::globalscope::GlobalScope;
|
use crate::dom::globalscope::GlobalScope;
|
||||||
use crate::script_runtime::JSContext;
|
use crate::script_runtime::JSContext;
|
||||||
|
@ -49,6 +50,7 @@ pub struct Gamepad {
|
||||||
axis_bounds: (f64, f64),
|
axis_bounds: (f64, f64),
|
||||||
button_bounds: (f64, f64),
|
button_bounds: (f64, f64),
|
||||||
exposed: Cell<bool>,
|
exposed: Cell<bool>,
|
||||||
|
vibration_actuator: Dom<GamepadHapticActuator>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Gamepad {
|
impl Gamepad {
|
||||||
|
@ -65,6 +67,7 @@ impl Gamepad {
|
||||||
hand: GamepadHand,
|
hand: GamepadHand,
|
||||||
axis_bounds: (f64, f64),
|
axis_bounds: (f64, f64),
|
||||||
button_bounds: (f64, f64),
|
button_bounds: (f64, f64),
|
||||||
|
vibration_actuator: &GamepadHapticActuator,
|
||||||
) -> Gamepad {
|
) -> Gamepad {
|
||||||
Self {
|
Self {
|
||||||
reflector_: Reflector::new(),
|
reflector_: Reflector::new(),
|
||||||
|
@ -81,6 +84,7 @@ impl Gamepad {
|
||||||
axis_bounds,
|
axis_bounds,
|
||||||
button_bounds,
|
button_bounds,
|
||||||
exposed: Cell::new(false),
|
exposed: Cell::new(false),
|
||||||
|
vibration_actuator: Dom::from_ref(vibration_actuator),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,8 +94,16 @@ impl Gamepad {
|
||||||
id: String,
|
id: String,
|
||||||
axis_bounds: (f64, f64),
|
axis_bounds: (f64, f64),
|
||||||
button_bounds: (f64, f64),
|
button_bounds: (f64, f64),
|
||||||
|
supported_haptic_effects: GamepadSupportedHapticEffects,
|
||||||
) -> DomRoot<Gamepad> {
|
) -> 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
|
/// When we construct a new gamepad, we initialize the number of buttons and
|
||||||
|
@ -105,8 +117,11 @@ impl Gamepad {
|
||||||
id: String,
|
id: String,
|
||||||
axis_bounds: (f64, f64),
|
axis_bounds: (f64, f64),
|
||||||
button_bounds: (f64, f64),
|
button_bounds: (f64, f64),
|
||||||
|
supported_haptic_effects: GamepadSupportedHapticEffects,
|
||||||
) -> DomRoot<Gamepad> {
|
) -> DomRoot<Gamepad> {
|
||||||
let button_list = GamepadButtonList::init_buttons(global);
|
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(
|
let gamepad = reflect_dom_object_with_proto(
|
||||||
Box::new(Gamepad::new_inherited(
|
Box::new(Gamepad::new_inherited(
|
||||||
gamepad_id,
|
gamepad_id,
|
||||||
|
@ -120,6 +135,7 @@ impl Gamepad {
|
||||||
GamepadHand::_empty,
|
GamepadHand::_empty,
|
||||||
axis_bounds,
|
axis_bounds,
|
||||||
button_bounds,
|
button_bounds,
|
||||||
|
&vibration_actuator,
|
||||||
)),
|
)),
|
||||||
global,
|
global,
|
||||||
None,
|
None,
|
||||||
|
@ -165,6 +181,11 @@ impl GamepadMethods for Gamepad {
|
||||||
DomRoot::from_ref(&*self.buttons)
|
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
|
// https://w3c.github.io/gamepad/extensions.html#gamepadhand-enum
|
||||||
fn Hand(&self) -> GamepadHand {
|
fn Hand(&self) -> GamepadHand {
|
||||||
self.hand
|
self.hand
|
||||||
|
@ -286,6 +307,10 @@ impl Gamepad {
|
||||||
pub fn set_exposed(&self, exposed: bool) {
|
pub fn set_exposed(&self, exposed: bool) {
|
||||||
self.exposed.set(exposed);
|
self.exposed.set(exposed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn vibration_actuator(&self) -> &GamepadHapticActuator {
|
||||||
|
&*self.vibration_actuator
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <https://www.w3.org/TR/gamepad/#dfn-gamepad-user-gesture>
|
/// <https://www.w3.org/TR/gamepad/#dfn-gamepad-user-gesture>
|
||||||
|
|
388
components/script/dom/gamepadhapticactuator.rs
Normal file
388
components/script/dom/gamepadhapticactuator.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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::serializable::{BlobData, BlobImpl, FileBlob};
|
||||||
use script_traits::transferable::MessagePortImpl;
|
use script_traits::transferable::MessagePortImpl;
|
||||||
use script_traits::{
|
use script_traits::{
|
||||||
BroadcastMsg, GamepadEvent, GamepadUpdateType, MessagePortMsg, MsDuration, PortMessageTask,
|
BroadcastMsg, GamepadEvent, GamepadSupportedHapticEffects, GamepadUpdateType, MessagePortMsg,
|
||||||
ScriptMsg, ScriptToConstellationChan, TimerEvent, TimerEventId, TimerSchedulerMsg, TimerSource,
|
MsDuration, PortMessageTask, ScriptMsg, ScriptToConstellationChan, TimerEvent, TimerEventId,
|
||||||
|
TimerSchedulerMsg, TimerSource,
|
||||||
};
|
};
|
||||||
use servo_url::{ImmutableOrigin, MutableOrigin, ServoUrl};
|
use servo_url::{ImmutableOrigin, MutableOrigin, ServoUrl};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
@ -3140,12 +3141,13 @@ impl GlobalScope {
|
||||||
|
|
||||||
pub fn handle_gamepad_event(&self, gamepad_event: GamepadEvent) {
|
pub fn handle_gamepad_event(&self, gamepad_event: GamepadEvent) {
|
||||||
match gamepad_event {
|
match gamepad_event {
|
||||||
GamepadEvent::Connected(index, name, bounds) => {
|
GamepadEvent::Connected(index, name, bounds, supported_haptic_effects) => {
|
||||||
self.handle_gamepad_connect(
|
self.handle_gamepad_connect(
|
||||||
index.0,
|
index.0,
|
||||||
name,
|
name,
|
||||||
bounds.axis_bounds,
|
bounds.axis_bounds,
|
||||||
bounds.button_bounds,
|
bounds.button_bounds,
|
||||||
|
supported_haptic_effects,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
GamepadEvent::Disconnected(index) => {
|
GamepadEvent::Disconnected(index) => {
|
||||||
|
@ -3167,6 +3169,7 @@ impl GlobalScope {
|
||||||
name: String,
|
name: String,
|
||||||
axis_bounds: (f64, f64),
|
axis_bounds: (f64, f64),
|
||||||
button_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,
|
// TODO: 2. If document is not null and is not allowed to use the "gamepad" permission,
|
||||||
// then abort these steps.
|
// then abort these steps.
|
||||||
|
@ -3178,7 +3181,9 @@ impl GlobalScope {
|
||||||
if let Some(window) = global.downcast::<Window>() {
|
if let Some(window) = global.downcast::<Window>() {
|
||||||
let navigator = window.Navigator();
|
let navigator = window.Navigator();
|
||||||
let selected_index = navigator.select_gamepad_index();
|
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);
|
navigator.set_gamepad(selected_index as usize, &gamepad);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -321,6 +321,7 @@ pub mod gamepad;
|
||||||
pub mod gamepadbutton;
|
pub mod gamepadbutton;
|
||||||
pub mod gamepadbuttonlist;
|
pub mod gamepadbuttonlist;
|
||||||
pub mod gamepadevent;
|
pub mod gamepadevent;
|
||||||
|
pub mod gamepadhapticactuator;
|
||||||
pub mod gamepadpose;
|
pub mod gamepadpose;
|
||||||
pub mod globalscope;
|
pub mod globalscope;
|
||||||
pub mod gpu;
|
pub mod gpu;
|
||||||
|
|
|
@ -12,6 +12,7 @@ interface Gamepad {
|
||||||
readonly attribute DOMString mapping;
|
readonly attribute DOMString mapping;
|
||||||
readonly attribute Float64Array axes;
|
readonly attribute Float64Array axes;
|
||||||
[SameObject] readonly attribute GamepadButtonList buttons;
|
[SameObject] readonly attribute GamepadButtonList buttons;
|
||||||
|
[SameObject] readonly attribute GamepadHapticActuator vibrationActuator;
|
||||||
};
|
};
|
||||||
|
|
||||||
// https://w3c.github.io/gamepad/extensions.html#partial-gamepad-interface
|
// https://w3c.github.io/gamepad/extensions.html#partial-gamepad-interface
|
||||||
|
|
38
components/script/dom/webidls/GamepadHapticActuator.webidl
Normal file
38
components/script/dom/webidls/GamepadHapticActuator.webidl
Normal 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;
|
||||||
|
};
|
|
@ -214,6 +214,10 @@ pub enum EmbedderMsg {
|
||||||
ReadyToPresent(Vec<WebViewId>),
|
ReadyToPresent(Vec<WebViewId>),
|
||||||
/// The given event was delivered to a pipeline in the given browser.
|
/// The given event was delivered to a pipeline in the given browser.
|
||||||
EventDelivered(CompositorEventVariant),
|
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.
|
/// The variant of CompositorEvent that was delivered to a pipeline.
|
||||||
|
@ -268,6 +272,8 @@ impl Debug for EmbedderMsg {
|
||||||
EmbedderMsg::ShowContextMenu(..) => write!(f, "ShowContextMenu"),
|
EmbedderMsg::ShowContextMenu(..) => write!(f, "ShowContextMenu"),
|
||||||
EmbedderMsg::ReadyToPresent(..) => write!(f, "ReadyToPresent"),
|
EmbedderMsg::ReadyToPresent(..) => write!(f, "ReadyToPresent"),
|
||||||
EmbedderMsg::EventDelivered(..) => write!(f, "HitTestedEvent"),
|
EmbedderMsg::EventDelivered(..) => write!(f, "HitTestedEvent"),
|
||||||
|
EmbedderMsg::PlayGamepadHapticEffect(..) => write!(f, "PlayGamepadHapticEffect"),
|
||||||
|
EmbedderMsg::StopGamepadHapticEffect(..) => write!(f, "StopGamepadHapticEffect"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -388,3 +394,18 @@ pub enum InputMethodType {
|
||||||
Url,
|
Url,
|
||||||
Week,
|
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),
|
||||||
|
}
|
||||||
|
|
|
@ -1092,12 +1092,26 @@ pub struct GamepadInputBounds {
|
||||||
pub button_bounds: (f64, f64),
|
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)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
/// The type of Gamepad event
|
/// The type of Gamepad event
|
||||||
pub enum GamepadEvent {
|
pub enum GamepadEvent {
|
||||||
/// A new gamepad has been connected
|
/// A new gamepad has been connected
|
||||||
/// <https://www.w3.org/TR/gamepad/#event-gamepadconnected>
|
/// <https://www.w3.org/TR/gamepad/#event-gamepadconnected>
|
||||||
Connected(GamepadIndex, String, GamepadInputBounds),
|
Connected(
|
||||||
|
GamepadIndex,
|
||||||
|
String,
|
||||||
|
GamepadInputBounds,
|
||||||
|
GamepadSupportedHapticEffects,
|
||||||
|
),
|
||||||
/// An existing gamepad has been disconnected
|
/// An existing gamepad has been disconnected
|
||||||
/// <https://www.w3.org/TR/gamepad/#event-gamepaddisconnected>
|
/// <https://www.w3.org/TR/gamepad/#event-gamepaddisconnected>
|
||||||
Disconnected(GamepadIndex),
|
Disconnected(GamepadIndex),
|
||||||
|
|
|
@ -98,7 +98,7 @@ egui = { version = "0.28.1" }
|
||||||
egui_glow = { version = "0.28.1", features = ["winit"] }
|
egui_glow = { version = "0.28.1", features = ["winit"] }
|
||||||
egui-winit = { version = "0.28.1", default-features = false, features = ["clipboard", "wayland"] }
|
egui-winit = { version = "0.28.1", default-features = false, features = ["clipboard", "wayland"] }
|
||||||
euclid = { workspace = true }
|
euclid = { workspace = true }
|
||||||
gilrs = "0.10.8"
|
gilrs = { git = "https://gitlab.com/gilrs-project/gilrs", rev = "eafb7f2ef488874188c5d75adce9aef486be9d4e" }
|
||||||
gleam = { workspace = true }
|
gleam = { workspace = true }
|
||||||
glow = "0.13.1"
|
glow = "0.13.1"
|
||||||
keyboard-types = { workspace = true }
|
keyboard-types = { workspace = true }
|
||||||
|
|
|
@ -176,6 +176,8 @@ mod from_servo {
|
||||||
Self::OnDevtoolsStarted(..) => target!("OnDevtoolsStarted"),
|
Self::OnDevtoolsStarted(..) => target!("OnDevtoolsStarted"),
|
||||||
Self::ReadyToPresent(..) => target!("ReadyToPresent"),
|
Self::ReadyToPresent(..) => target!("ReadyToPresent"),
|
||||||
Self::EventDelivered(..) => target!("EventDelivered"),
|
Self::EventDelivered(..) => target!("EventDelivered"),
|
||||||
|
Self::PlayGamepadHapticEffect(..) => target!("PlayGamepadHapticEffect"),
|
||||||
|
Self::StopGamepadHapticEffect(..) => target!("StopGamepadHapticEffect"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,18 +12,21 @@ use std::{env, thread};
|
||||||
|
|
||||||
use arboard::Clipboard;
|
use arboard::Clipboard;
|
||||||
use euclid::{Point2D, Vector2D};
|
use euclid::{Point2D, Vector2D};
|
||||||
|
use gilrs::ff::{BaseEffect, BaseEffectType, Effect, EffectBuilder, Repeat, Replay, Ticks};
|
||||||
use gilrs::{EventType, Gilrs};
|
use gilrs::{EventType, Gilrs};
|
||||||
use keyboard_types::{Key, KeyboardEvent, Modifiers, ShortcutMatcher};
|
use keyboard_types::{Key, KeyboardEvent, Modifiers, ShortcutMatcher};
|
||||||
use log::{debug, error, info, trace, warn};
|
use log::{debug, error, info, trace, warn};
|
||||||
use servo::base::id::TopLevelBrowsingContextId as WebViewId;
|
use servo::base::id::TopLevelBrowsingContextId as WebViewId;
|
||||||
use servo::compositing::windowing::{EmbedderEvent, WebRenderDebugOption};
|
use servo::compositing::windowing::{EmbedderEvent, WebRenderDebugOption};
|
||||||
use servo::embedder_traits::{
|
use servo::embedder_traits::{
|
||||||
CompositorEventVariant, ContextMenuResult, EmbedderMsg, FilterPattern, PermissionPrompt,
|
CompositorEventVariant, ContextMenuResult, DualRumbleEffectParams, EmbedderMsg, FilterPattern,
|
||||||
PermissionRequest, PromptDefinition, PromptOrigin, PromptResult,
|
GamepadHapticEffectType, PermissionPrompt, PermissionRequest, PromptDefinition, PromptOrigin,
|
||||||
|
PromptResult,
|
||||||
};
|
};
|
||||||
|
use servo::ipc_channel::ipc::IpcSender;
|
||||||
use servo::script_traits::{
|
use servo::script_traits::{
|
||||||
GamepadEvent, GamepadIndex, GamepadInputBounds, GamepadUpdateType, TouchEventType,
|
GamepadEvent, GamepadIndex, GamepadInputBounds, GamepadSupportedHapticEffects,
|
||||||
TraversalDirection,
|
GamepadUpdateType, TouchEventType, TraversalDirection,
|
||||||
};
|
};
|
||||||
use servo::servo_config::opts;
|
use servo::servo_config::opts;
|
||||||
use servo::servo_url::ServoUrl;
|
use servo::servo_url::ServoUrl;
|
||||||
|
@ -59,6 +62,7 @@ pub struct WebViewManager<Window: WindowPortsMethods + ?Sized> {
|
||||||
event_queue: Vec<EmbedderEvent>,
|
event_queue: Vec<EmbedderEvent>,
|
||||||
clipboard: Option<Clipboard>,
|
clipboard: Option<Clipboard>,
|
||||||
gamepad: Option<Gilrs>,
|
gamepad: Option<Gilrs>,
|
||||||
|
haptic_effects: HashMap<usize, HapticEffect>,
|
||||||
shutdown_requested: bool,
|
shutdown_requested: bool,
|
||||||
load_status: LoadStatus,
|
load_status: LoadStatus,
|
||||||
}
|
}
|
||||||
|
@ -80,6 +84,11 @@ pub enum LoadStatus {
|
||||||
LoadComplete,
|
LoadComplete,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct HapticEffect {
|
||||||
|
pub effect: Effect,
|
||||||
|
pub sender: IpcSender<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
impl<Window> WebViewManager<Window>
|
impl<Window> WebViewManager<Window>
|
||||||
where
|
where
|
||||||
Window: WindowPortsMethods + ?Sized,
|
Window: WindowPortsMethods + ?Sized,
|
||||||
|
@ -108,6 +117,8 @@ where
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
haptic_effects: HashMap::default(),
|
||||||
|
|
||||||
event_queue: Vec::new(),
|
event_queue: Vec::new(),
|
||||||
shutdown_requested: false,
|
shutdown_requested: false,
|
||||||
load_status: LoadStatus::LoadComplete,
|
load_status: LoadStatus::LoadComplete,
|
||||||
|
@ -218,11 +229,32 @@ where
|
||||||
axis_bounds: (-1.0, 1.0),
|
axis_bounds: (-1.0, 1.0),
|
||||||
button_bounds: (0.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 => {
|
EventType::Disconnected => {
|
||||||
gamepad_event = Some(GamepadEvent::Disconnected(index));
|
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 {
|
pub fn shutdown_requested(&self) -> bool {
|
||||||
self.shutdown_requested
|
self.shutdown_requested
|
||||||
}
|
}
|
||||||
|
@ -744,6 +849,19 @@ where
|
||||||
.push(EmbedderEvent::FocusWebView(webview_id));
|
.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");
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -623,7 +623,9 @@ impl ServoGlue {
|
||||||
EmbedderMsg::HeadParsed |
|
EmbedderMsg::HeadParsed |
|
||||||
EmbedderMsg::SetFullscreenState(..) |
|
EmbedderMsg::SetFullscreenState(..) |
|
||||||
EmbedderMsg::ReportProfile(..) |
|
EmbedderMsg::ReportProfile(..) |
|
||||||
EmbedderMsg::EventDelivered(..) => {},
|
EmbedderMsg::EventDelivered(..) |
|
||||||
|
EmbedderMsg::PlayGamepadHapticEffect(..) |
|
||||||
|
EmbedderMsg::StopGamepadHapticEffect(..) => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,28 +1,4 @@
|
||||||
[idlharness.window.html]
|
[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)]
|
[GamepadHapticActuator interface: operation playEffect(GamepadHapticEffectType, optional GamepadEffectParameters)]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
||||||
|
|
24
tests/wpt/meta/gamepad/idlharness.window.js.ini
vendored
24
tests/wpt/meta/gamepad/idlharness.window.js.ini
vendored
|
@ -1,28 +1,4 @@
|
||||||
[idlharness.window.html]
|
[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)]
|
[GamepadHapticActuator interface: operation playEffect(GamepadHapticEffectType, optional GamepadEffectParameters)]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue