mirror of
https://github.com/servo/servo.git
synced 2025-06-06 16:45:39 +00: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]]
|
||||
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",
|
||||
|
|
|
@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -176,6 +176,10 @@ DOMInterfaces = {
|
|||
'CreateRenderPipelineAsync',
|
||||
'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
|
||||
// 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"));
|
||||
|
|
|
@ -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>
|
||||
|
|
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::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);
|
||||
}
|
||||
}),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
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>),
|
||||
/// 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),
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -623,7 +623,9 @@ impl ServoGlue {
|
|||
EmbedderMsg::HeadParsed |
|
||||
EmbedderMsg::SetFullscreenState(..) |
|
||||
EmbedderMsg::ReportProfile(..) |
|
||||
EmbedderMsg::EventDelivered(..) => {},
|
||||
EmbedderMsg::EventDelivered(..) |
|
||||
EmbedderMsg::PlayGamepadHapticEffect(..) |
|
||||
EmbedderMsg::StopGamepadHapticEffect(..) => {},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
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]
|
||||
[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
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue