Expose a GamepadProvider and use Responder types for messages (#41568)

Expose a `GamepadProvider` and use `Responder` types for messages

Testing: 
Fixes: https://github.com/servo/servo/issues/41453

---------

Signed-off-by: atbrakhi <atbrakhi@igalia.com>
This commit is contained in:
atbrakhi
2026-02-01 18:54:01 +05:30
committed by GitHub
parent 0f9f601eb9
commit b6a1761198
11 changed files with 251 additions and 169 deletions

View File

@@ -0,0 +1,67 @@
/* 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 embedder_traits::GamepadHapticEffectType;
pub enum GamepadHapticEffectRequestType {
Play(GamepadHapticEffectType),
Stop,
}
pub struct GamepadHapticEffectRequest {
gamepad_index: usize,
request_type: GamepadHapticEffectRequestType,
callback: Option<Box<dyn FnOnce(bool)>>,
}
impl GamepadHapticEffectRequest {
pub(crate) fn new(
gamepad_index: usize,
request_type: GamepadHapticEffectRequestType,
callback: Box<dyn FnOnce(bool)>,
) -> Self {
Self {
gamepad_index,
request_type,
callback: Some(callback),
}
}
pub fn gamepad_index(&self) -> usize {
self.gamepad_index
}
pub fn request_type(&self) -> &GamepadHapticEffectRequestType {
&self.request_type
}
pub fn failed(mut self) {
if let Some(callback) = self.callback.take() {
callback(false);
}
}
pub fn succeeded(mut self) {
if let Some(callback) = self.callback.take() {
callback(true);
}
}
}
impl Drop for GamepadHapticEffectRequest {
fn drop(&mut self) {
if let Some(callback) = self.callback.take() {
callback(false);
}
}
}
pub trait GamepadProvider {
/// Handle a request to play or stop a haptic effect on a connected gamepad.
fn handle_haptic_effect_request(&self, _request: GamepadHapticEffectRequest) {}
}
pub(crate) struct DefaultGamepadProvider;
impl GamepadProvider for DefaultGamepadProvider {}

View File

@@ -13,6 +13,8 @@
//! `ScriptThread` and the `LayoutThread`, as well maintains the navigation context.
mod clipboard_delegate;
#[cfg(feature = "gamepad")]
mod gamepad_provider;
mod javascript_evaluator;
mod network_manager;
mod proxies;
@@ -61,6 +63,10 @@ pub use webrender_api::units::{
DeviceIntPoint, DeviceIntRect, DeviceIntSize, DevicePixel, DevicePoint, DeviceVector2D,
};
#[cfg(feature = "gamepad")]
pub use crate::gamepad_provider::{
GamepadHapticEffectRequest, GamepadHapticEffectRequestType, GamepadProvider,
};
pub use crate::network_manager::{CacheEntry, NetworkManager};
pub use crate::servo::{Servo, ServoBuilder, run_content_process};
pub use crate::servo_delegate::{ServoDelegate, ServoError};

View File

@@ -74,6 +74,8 @@ use storage_traits::StorageThreads;
use style::global_style_data::StyleThreadPool;
use crate::clipboard_delegate::StringRequest;
#[cfg(feature = "gamepad")]
use crate::gamepad_provider::{GamepadHapticEffectRequest, GamepadHapticEffectRequestType};
use crate::javascript_evaluator::JavaScriptEvaluator;
use crate::network_manager::NetworkManager;
use crate::proxies::ConstellationProxy;
@@ -586,30 +588,35 @@ impl ServoInner {
callback,
) => {
if let Some(webview) = self.get_webview_handle(webview_id) {
webview.delegate().play_gamepad_haptic_effect(
webview,
let request = GamepadHapticEffectRequest::new(
gamepad_index,
gamepad_haptic_effect_type,
GamepadHapticEffectRequestType::Play(gamepad_haptic_effect_type),
Box::new(move |success| {
callback
.send(success)
.expect("Could not send message via callback")
}),
);
webview
.gamepad_provider()
.handle_haptic_effect_request(request);
}
},
#[cfg(feature = "gamepad")]
EmbedderMsg::StopGamepadHapticEffect(webview_id, gamepad_index, callback) => {
if let Some(webview) = self.get_webview_handle(webview_id) {
webview.delegate().stop_gamepad_haptic_effect(
webview,
let request = GamepadHapticEffectRequest::new(
gamepad_index,
GamepadHapticEffectRequestType::Stop,
Box::new(move |success| {
callback
.send(success)
.expect("Could not send message via callback")
}),
);
webview
.gamepad_provider()
.handle_haptic_effect_request(request);
}
},
EmbedderMsg::ShowNotification(webview_id, notification) => {

View File

@@ -28,6 +28,8 @@ use url::Url;
use webrender_api::units::{DeviceIntRect, DevicePixel, DevicePoint, DeviceSize};
use crate::clipboard_delegate::{ClipboardDelegate, DefaultClipboardDelegate};
#[cfg(feature = "gamepad")]
use crate::gamepad_provider::{DefaultGamepadProvider, GamepadProvider};
use crate::responders::IpcResponder;
use crate::webview_delegate::{CreateNewWebViewRequest, DefaultWebViewDelegate, WebViewDelegate};
use crate::{
@@ -84,6 +86,8 @@ pub(crate) struct WebViewInner {
pub(crate) servo: Servo,
pub(crate) delegate: Rc<dyn WebViewDelegate>,
pub(crate) clipboard_delegate: Rc<dyn ClipboardDelegate>,
#[cfg(feature = "gamepad")]
pub(crate) gamepad_provider: Rc<dyn GamepadProvider>,
rendering_context: Rc<dyn RenderingContext>,
user_content_manager: Option<Rc<UserContentManager>>,
@@ -126,6 +130,8 @@ impl WebView {
rendering_context: builder.rendering_context,
delegate: builder.delegate,
clipboard_delegate: Rc::new(DefaultClipboardDelegate),
#[cfg(feature = "gamepad")]
gamepad_provider: Rc::new(DefaultGamepadProvider),
hidpi_scale_factor: builder.hidpi_scale_factor,
load_status: LoadStatus::Started,
status_text: None,
@@ -245,6 +251,16 @@ impl WebView {
self.inner_mut().clipboard_delegate = delegate;
}
#[cfg(feature = "gamepad")]
pub fn gamepad_provider(&self) -> Rc<dyn GamepadProvider> {
self.inner().gamepad_provider.clone()
}
#[cfg(feature = "gamepad")]
pub fn set_gamepad_provider(&self, provider: Rc<dyn GamepadProvider>) {
self.inner_mut().gamepad_provider = provider;
}
pub fn id(&self) -> WebViewId {
self.inner().id
}

View File

@@ -28,6 +28,8 @@ use crate::desktop::tracing::trace_winit_event;
use crate::parser::get_default_url;
use crate::prefs::ServoShellPreferences;
use crate::running_app_state::RunningAppState;
#[cfg(feature = "gamepad")]
use crate::running_app_state::ServoshellGamepadProvider;
use crate::window::{PlatformWindow, ServoShellWindowId};
pub(crate) enum AppState {
@@ -123,6 +125,8 @@ impl App {
self.waker.clone(),
user_content_manager,
self.preferences.clone(),
#[cfg(feature = "gamepad")]
ServoshellGamepadProvider::maybe_new().map(Rc::new),
));
running_state.open_window(platform_window, self.initial_url.as_url().clone());

View File

@@ -2,27 +2,29 @@
* 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::RefCell;
use std::collections::HashMap;
use gilrs::ff::{BaseEffect, BaseEffectType, Effect, EffectBuilder, Repeat, Replay, Ticks};
use gilrs::{EventType, Gilrs};
use log::{debug, warn};
use servo::{
GamepadEvent, GamepadHapticEffectType, GamepadIndex, GamepadInputBounds,
GamepadEvent, GamepadHapticEffectRequest, GamepadHapticEffectRequestType,
GamepadHapticEffectType, GamepadIndex, GamepadInputBounds, GamepadProvider,
GamepadSupportedHapticEffects, GamepadUpdateType, InputEvent, WebView,
};
pub struct HapticEffect {
pub effect: Effect,
pub callback: Option<Box<dyn FnOnce(bool)>>,
pub request: GamepadHapticEffectRequest,
}
pub(crate) struct GamepadSupport {
handle: Gilrs,
haptic_effects: HashMap<usize, HapticEffect>,
pub(crate) struct ServoshellGamepadProvider {
handle: RefCell<Gilrs>,
haptic_effects: RefCell<HashMap<usize, HapticEffect>>,
}
impl GamepadSupport {
impl ServoshellGamepadProvider {
pub(crate) fn maybe_new() -> Option<Self> {
let handle = match Gilrs::new() {
Ok(handle) => handle,
@@ -32,15 +34,16 @@ impl GamepadSupport {
},
};
Some(Self {
handle,
haptic_effects: Default::default(),
handle: RefCell::new(handle),
haptic_effects: RefCell::new(Default::default()),
})
}
/// Handle updates to connected gamepads from GilRs
pub(crate) fn handle_gamepad_events(&mut self, active_webview: WebView) {
while let Some(event) = self.handle.next_event() {
let gamepad = self.handle.gamepad(event.id);
pub(crate) fn handle_gamepad_events(&self, active_webview: WebView) {
let mut handle = self.handle.borrow_mut();
while let Some(event) = handle.next_event() {
let gamepad = handle.gamepad(event.id);
let name = gamepad.name();
let index = GamepadIndex(event.id.into());
let mut gamepad_event: Option<GamepadEvent> = None;
@@ -113,14 +116,13 @@ impl GamepadSupport {
gamepad_event = Some(GamepadEvent::Disconnected(index));
},
EventType::ForceFeedbackEffectCompleted => {
let Some(effect) = self.haptic_effects.get_mut(&event.id.into()) else {
if let Some(haptic_effect) =
self.haptic_effects.borrow_mut().remove(&event.id.into())
{
haptic_effect.request.succeeded();
} else {
warn!("Failed to find haptic effect for id {}", event.id);
return;
};
if let Some(callback) = effect.callback.take() {
callback(true);
}
self.haptic_effects.remove(&event.id.into());
},
_ => {},
}
@@ -156,62 +158,71 @@ impl GamepadSupport {
}
}
pub(crate) fn play_haptic_effect(
&mut self,
index: usize,
effect_type: GamepadHapticEffectType,
callback: Box<dyn FnOnce(bool)>,
fn play_haptic_effect(
&self,
effect_type: &GamepadHapticEffectType,
request: GamepadHapticEffectRequest,
) {
let index = request.gamepad_index();
let GamepadHapticEffectType::DualRumble(params) = effect_type;
if let Some(connected_gamepad) = self
.handle
let mut handle = self.handle.borrow_mut();
let Some(connected_gamepad) = handle
.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(&mut self.handle)
.expect("Failed to create haptic effect, ensure connected gamepad supports force feedback.");
self.haptic_effects.insert(
index,
HapticEffect {
effect,
callback: Some(callback),
},
);
self.haptic_effects[&index]
.effect
.play()
.expect("Failed to play haptic effect.");
} else {
else {
debug!("Couldn't find connected gamepad to play haptic effect on");
}
request.failed();
return;
};
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(&mut handle)
.expect(
"Failed to create haptic effect, ensure connected gamepad supports force feedback.",
);
let mut haptic_effects = self.haptic_effects.borrow_mut();
haptic_effects.insert(index, HapticEffect { effect, request });
haptic_effects[&index]
.effect
.play()
.expect("Failed to play haptic effect.");
}
pub(crate) fn stop_haptic_effect(&mut self, index: usize) -> bool {
let Some(haptic_effect) = self.haptic_effects.get(&index) else {
return false;
fn stop_haptic_effect(&self, request: GamepadHapticEffectRequest) {
let index = request.gamepad_index();
let mut haptic_effects = self.haptic_effects.borrow_mut();
let Some(haptic_effect) = haptic_effects.get(&index) else {
request.failed();
return;
};
let stopped_successfully = match haptic_effect.effect.stop() {
@@ -221,8 +232,25 @@ impl GamepadSupport {
false
},
};
self.haptic_effects.remove(&index);
haptic_effects.remove(&index);
stopped_successfully
if stopped_successfully {
request.succeeded();
} else {
request.failed();
}
}
}
impl GamepadProvider for ServoshellGamepadProvider {
fn handle_haptic_effect_request(&self, request: GamepadHapticEffectRequest) {
match request.request_type() {
GamepadHapticEffectRequestType::Play(effect_type) => {
self.play_haptic_effect(&effect_type.clone(), request);
},
GamepadHapticEffectRequestType::Stop => {
self.stop_haptic_effect(request);
},
}
}
}

View File

@@ -1,33 +0,0 @@
/* 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 servo::{GamepadHapticEffectType, WebView};
/// A dummy version of [`crate::desktop::GamepadSupport`] used to avoid conditional compilation in
/// servoshell and as a skeleton to implement gamepad support for platforms that do not
/// currently support it.
pub(crate) struct GamepadSupport;
impl GamepadSupport {
pub(crate) fn maybe_new() -> Option<Self> {
None
}
pub(crate) fn handle_gamepad_events(&mut self, _active_webview: WebView) {
unreachable!("Dummy gamepad support should never be called.");
}
pub(crate) fn play_haptic_effect(
&mut self,
_index: usize,
_effect_type: GamepadHapticEffectType,
_effect_complete_callback: Box<dyn FnOnce(bool)>,
) {
unreachable!("Dummy gamepad support should never be called.");
}
pub(crate) fn stop_haptic_effect(&mut self, _index: usize) -> bool {
unreachable!("Dummy gamepad support should never be called.");
}
}

View File

@@ -5,8 +5,6 @@
#[cfg(target_os = "android")]
mod android;
pub(crate) mod app;
#[cfg(feature = "gamepad")]
pub(crate) mod gamepad;
mod host_trait;
mod log;
#[cfg(target_env = "ohos")]

View File

@@ -25,14 +25,6 @@ mod running_app_state;
mod webdriver;
mod window;
#[cfg(all(
feature = "gamepad",
not(any(target_os = "android", target_env = "ohos"))
))]
pub(crate) use crate::desktop::gamepad::GamepadSupport;
#[cfg(all(feature = "gamepad", any(target_os = "android", target_env = "ohos")))]
pub(crate) use crate::egl::gamepad::GamepadSupport;
pub mod platform {
#[cfg(target_os = "macos")]
pub use crate::platform::macos::deinit;

View File

@@ -13,8 +13,6 @@ use crossbeam_channel::{Receiver, Sender, unbounded};
use euclid::Rect;
use image::{DynamicImage, ImageFormat, RgbaImage};
use log::{error, info, warn};
#[cfg(feature = "gamepad")]
use servo::GamepadHapticEffectType;
use servo::{
AllowOrDenyRequest, AuthenticationRequest, CSSPixel, ConsoleLogLevel, CreateNewWebViewRequest,
DeviceIntPoint, DeviceIntSize, EmbedderControl, EmbedderControlId, EventLoopWaker,
@@ -26,8 +24,11 @@ use servo::{
};
use url::Url;
#[cfg(feature = "gamepad")]
use crate::GamepadSupport;
#[cfg(all(
feature = "gamepad",
not(any(target_os = "android", target_env = "ohos"))
))]
pub(crate) use crate::desktop::gamepad::ServoshellGamepadProvider;
use crate::prefs::{EXPERIMENTAL_PREFS, ServoShellPreferences};
use crate::webdriver::WebDriverEmbedderControls;
use crate::window::{PlatformWindow, ServoShellWindow, ServoShellWindowId};
@@ -148,9 +149,13 @@ pub(crate) enum UserInterfaceCommand {
}
pub(crate) struct RunningAppState {
/// Gamepad support, which may be `None` if it failed to initialize.
#[cfg(feature = "gamepad")]
gamepad_support: RefCell<Option<GamepadSupport>>,
/// The gamepad provider, used for handling gamepad events and set on each WebView.
/// May be `None` if gamepad support is disabled or failed to initialize.
#[cfg(all(
feature = "gamepad",
not(any(target_os = "android", target_env = "ohos"))
))]
gamepad_provider: Option<Rc<ServoshellGamepadProvider>>,
/// The [`WebDriverSenders`] used to reply to pending WebDriver requests.
pub(crate) webdriver_senders: RefCell<WebDriverSenders>,
@@ -206,16 +211,14 @@ impl RunningAppState {
event_loop_waker: Box<dyn EventLoopWaker>,
user_content_manager: Rc<UserContentManager>,
default_preferences: Preferences,
#[cfg(all(
feature = "gamepad",
not(any(target_os = "android", target_env = "ohos"))
))]
gamepad_provider: Option<Rc<ServoshellGamepadProvider>>,
) -> Self {
servo.set_delegate(Rc::new(ServoShellServoDelegate));
#[cfg(feature = "gamepad")]
let gamepad_support = if pref!(dom_gamepad_enabled) {
GamepadSupport::maybe_new()
} else {
None
};
let webdriver_receiver = servoshell_preferences.webdriver_port.get().map(|port| {
let (embedder_sender, embedder_receiver) = unbounded();
webdriver_server::start_server(
@@ -233,8 +236,11 @@ impl RunningAppState {
Self {
windows: Default::default(),
focused_window: Default::default(),
#[cfg(feature = "gamepad")]
gamepad_support: RefCell::new(gamepad_support),
#[cfg(all(
feature = "gamepad",
not(any(target_os = "android", target_env = "ohos"))
))]
gamepad_provider,
webdriver_senders: RefCell::default(),
webdriver_embedder_controls: Default::default(),
pending_webdriver_events: Default::default(),
@@ -300,6 +306,14 @@ impl RunningAppState {
&self.servo
}
#[cfg(all(
feature = "gamepad",
not(any(target_os = "android", target_env = "ohos"))
))]
pub(crate) fn gamepad_provider(&self) -> Option<Rc<ServoshellGamepadProvider>> {
self.gamepad_provider.clone()
}
pub(crate) fn schedule_exit(&self) {
// When explicitly required to shutdown, unset webdriver port
// which allows normal shutdown.
@@ -361,7 +375,10 @@ impl RunningAppState {
self.handle_webdriver_messages(create_platform_window);
#[cfg(feature = "gamepad")]
#[cfg(all(
feature = "gamepad",
not(any(target_os = "android", target_env = "ohos"))
))]
if pref!(dom_gamepad_enabled) {
self.handle_gamepad_events();
}
@@ -565,17 +582,21 @@ impl RunningAppState {
webview.load(url);
}
#[cfg(feature = "gamepad")]
#[cfg(all(
feature = "gamepad",
not(any(target_os = "android", target_env = "ohos"))
))]
pub(crate) fn handle_gamepad_events(&self) {
let Some(gamepad_provider) = self.gamepad_provider.as_ref() else {
return;
};
let Some(active_webview) = self
.focused_window()
.and_then(|window| window.active_webview())
else {
return;
};
if let Some(gamepad_support) = self.gamepad_support.borrow_mut().as_mut() {
gamepad_support.handle_gamepad_events(active_webview);
}
gamepad_provider.handle_gamepad_events(active_webview);
}
pub(crate) fn handle_focused(&self, window: Rc<ServoShellWindow>) {
@@ -738,38 +759,6 @@ impl WebViewDelegate for RunningAppState {
self.window_for_webview_id(webview.id()).set_needs_repaint();
}
#[cfg(feature = "gamepad")]
fn play_gamepad_haptic_effect(
&self,
_webview: WebView,
index: usize,
effect_type: GamepadHapticEffectType,
effect_complete_callback: Box<dyn FnOnce(bool)>,
) {
match self.gamepad_support.borrow_mut().as_mut() {
Some(gamepad_support) => {
gamepad_support.play_haptic_effect(index, effect_type, effect_complete_callback);
},
None => {
effect_complete_callback(false);
},
}
}
#[cfg(feature = "gamepad")]
fn stop_gamepad_haptic_effect(
&self,
_webview: WebView,
index: usize,
haptic_stop_callback: Box<dyn FnOnce(bool)>,
) {
let stopped = match self.gamepad_support.borrow_mut().as_mut() {
Some(gamepad_support) => gamepad_support.stop_haptic_effect(index),
None => false,
};
haptic_stop_callback(stopped);
}
fn show_embedder_control(&self, webview: WebView, embedder_control: EmbedderControl) {
if self.servoshell_preferences.webdriver_port.get().is_some() {
if matches!(&embedder_control, EmbedderControl::SimpleDialog(..)) {

View File

@@ -92,6 +92,14 @@ impl ServoShellWindow {
.delegate(state.clone())
.build();
#[cfg(all(
feature = "gamepad",
not(any(target_os = "android", target_env = "ohos"))
))]
if let Some(gamepad_provider) = state.gamepad_provider() {
webview.set_gamepad_provider(gamepad_provider);
}
webview.notify_theme_change(self.platform_window.theme());
self.add_webview(webview.clone());
webview