mirror of
https://github.com/servo/servo.git
synced 2025-08-06 14:10:11 +01:00
servoshell: Port desktop servoshell to use delegate API (#35284)
Signed-off-by: Martin Robinson <mrobinson@igalia.com> Co-authored-by: Delan Azabani <dazabani@igalia.com> Co-authored-by: Mukilan Thiyagarajan <mukilan@igalia.com>
This commit is contained in:
parent
6b12499077
commit
5f08e4fa76
15 changed files with 1109 additions and 1258 deletions
|
@ -1089,7 +1089,11 @@ impl Servo {
|
|||
webview.delegate().notify_ready_to_show(webview);
|
||||
}
|
||||
},
|
||||
EmbedderMsg::WebViewClosed(_) => {},
|
||||
EmbedderMsg::WebViewClosed(webview_id) => {
|
||||
if let Some(webview) = self.get_webview_handle(webview_id) {
|
||||
webview.delegate().notify_closed(webview);
|
||||
}
|
||||
},
|
||||
EmbedderMsg::WebViewFocused(webview_id) => {
|
||||
for id in self.webviews.borrow().keys() {
|
||||
if let Some(webview) = self.get_webview_handle(*id) {
|
||||
|
|
|
@ -117,6 +117,10 @@ pub trait WebViewDelegate {
|
|||
/// The history state has changed.
|
||||
// changed pattern; maybe wasteful if embedder doesn’t care?
|
||||
fn notify_history_changed(&self, _webview: WebView, _: Vec<Url>, _: usize) {}
|
||||
/// Page content has closed this [`WebView`] via `window.close()`. It's the embedder's
|
||||
/// responsibility to remove the [`WebView`] from the interface when this notification
|
||||
/// occurs.
|
||||
fn notify_closed(&self, _webview: WebView) {}
|
||||
|
||||
/// A keyboard event has been sent to Servo, but remains unprocessed. This allows the
|
||||
/// embedding application to handle key events while first letting the [`WebView`]
|
||||
|
@ -144,10 +148,6 @@ pub trait WebViewDelegate {
|
|||
fn request_open_auxiliary_webview(&self, _parent_webview: WebView) -> Option<WebView> {
|
||||
None
|
||||
}
|
||||
/// Page content has requested that this [`WebView`] be closed. It's the embedder's
|
||||
/// responsibility to either ignore this request or to remove the [`WebView`] from the
|
||||
/// interface.
|
||||
fn request_close(&self, _webview: WebView) {}
|
||||
/// Open interface to request permission specified by prompt.
|
||||
fn request_permission(
|
||||
&self,
|
||||
|
|
|
@ -10,7 +10,6 @@ use std::rc::Rc;
|
|||
use std::time::Instant;
|
||||
use std::{env, fs};
|
||||
|
||||
use arboard::Clipboard;
|
||||
use log::{info, trace, warn};
|
||||
use raw_window_handle::HasDisplayHandle;
|
||||
use servo::compositing::windowing::{AnimationState, WindowMethods};
|
||||
|
@ -31,10 +30,11 @@ use winit::event::WindowEvent;
|
|||
use winit::event_loop::{ActiveEventLoop, ControlFlow};
|
||||
use winit::window::WindowId;
|
||||
|
||||
use super::app_state::AppState;
|
||||
use super::events_loop::{EventsLoop, WakerEvent};
|
||||
use super::minibrowser::{Minibrowser, MinibrowserEvent};
|
||||
use super::webview::WebViewManager;
|
||||
use super::{headed_window, headless_window};
|
||||
use crate::desktop::app_state::RunningAppState;
|
||||
use crate::desktop::embedder::{EmbedderCallbacks, XrDiscovery};
|
||||
use crate::desktop::tracing::trace_winit_event;
|
||||
use crate::desktop::window_trait::WindowPortsMethods;
|
||||
|
@ -45,9 +45,6 @@ pub struct App {
|
|||
opts: Opts,
|
||||
preferences: Preferences,
|
||||
servo_shell_preferences: ServoShellPreferences,
|
||||
clipboard: Option<Clipboard>,
|
||||
servo: Option<Servo>,
|
||||
webviews: WebViewManager,
|
||||
suspended: Cell<bool>,
|
||||
windows: HashMap<WindowId, Rc<dyn WindowPortsMethods>>,
|
||||
minibrowser: Option<Minibrowser>,
|
||||
|
@ -55,15 +52,16 @@ pub struct App {
|
|||
initial_url: ServoUrl,
|
||||
t_start: Instant,
|
||||
t: Instant,
|
||||
state: AppState,
|
||||
}
|
||||
|
||||
enum Present {
|
||||
pub(crate) enum Present {
|
||||
Deferred,
|
||||
None,
|
||||
}
|
||||
|
||||
/// Action to be taken by the caller of [`App::handle_events`].
|
||||
enum PumpResult {
|
||||
pub(crate) enum PumpResult {
|
||||
/// The caller should shut down Servo and its related context.
|
||||
Shutdown,
|
||||
Continue {
|
||||
|
@ -91,9 +89,6 @@ impl App {
|
|||
opts,
|
||||
preferences,
|
||||
servo_shell_preferences,
|
||||
clipboard: Clipboard::new().ok(),
|
||||
webviews: WebViewManager::new(),
|
||||
servo: None,
|
||||
suspended: Cell::new(false),
|
||||
windows: HashMap::new(),
|
||||
minibrowser: None,
|
||||
|
@ -101,6 +96,7 @@ impl App {
|
|||
initial_url: initial_url.clone(),
|
||||
t_start: t,
|
||||
t,
|
||||
state: AppState::Initializing,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -149,8 +145,6 @@ impl App {
|
|||
))
|
||||
};
|
||||
|
||||
// Create window's context
|
||||
self.webviews.set_window(window.clone());
|
||||
if window.winit_window().is_some() {
|
||||
self.minibrowser = Some(Minibrowser::new(
|
||||
&rendering_context,
|
||||
|
@ -159,17 +153,6 @@ impl App {
|
|||
));
|
||||
}
|
||||
|
||||
if let Some(ref mut minibrowser) = self.minibrowser {
|
||||
// Servo is not yet initialised, so there is no `servo_framebuffer_id`.
|
||||
minibrowser.update(
|
||||
window.winit_window().unwrap(),
|
||||
&mut self.webviews,
|
||||
self.servo.as_ref(),
|
||||
"init",
|
||||
);
|
||||
window.set_toolbar_height(minibrowser.toolbar_height);
|
||||
}
|
||||
|
||||
self.windows.insert(window.id(), window);
|
||||
|
||||
self.suspended.set(false);
|
||||
|
@ -192,6 +175,15 @@ impl App {
|
|||
None
|
||||
};
|
||||
|
||||
// Implements embedder methods, used by libservo and constellation.
|
||||
let embedder = Box::new(EmbedderCallbacks::new(self.waker.clone(), xr_discovery));
|
||||
|
||||
let composite_target = if self.minibrowser.is_some() {
|
||||
CompositeTarget::OffscreenFbo
|
||||
} else {
|
||||
CompositeTarget::ContextFbo
|
||||
};
|
||||
|
||||
// TODO: Remove this once dyn upcasting coercion stabilises
|
||||
// <https://github.com/rust-lang/rust/issues/65991>
|
||||
struct UpcastedWindow(Rc<dyn WindowPortsMethods>);
|
||||
|
@ -203,116 +195,61 @@ impl App {
|
|||
self.0.set_animation_state(state);
|
||||
}
|
||||
}
|
||||
let window = UpcastedWindow(window.clone());
|
||||
// Implements embedder methods, used by libservo and constellation.
|
||||
let embedder = Box::new(EmbedderCallbacks::new(self.waker.clone(), xr_discovery));
|
||||
|
||||
let composite_target = if self.minibrowser.is_some() {
|
||||
CompositeTarget::OffscreenFbo
|
||||
} else {
|
||||
CompositeTarget::ContextFbo
|
||||
};
|
||||
let servo = Servo::new(
|
||||
self.opts.clone(),
|
||||
self.preferences.clone(),
|
||||
Rc::new(rendering_context),
|
||||
embedder,
|
||||
Rc::new(window),
|
||||
Rc::new(UpcastedWindow(window.clone())),
|
||||
self.servo_shell_preferences.user_agent.clone(),
|
||||
composite_target,
|
||||
);
|
||||
|
||||
servo.setup_logging();
|
||||
|
||||
let webview = servo.new_webview(self.initial_url.clone().into_url());
|
||||
self.webviews.add(webview);
|
||||
let running_state = Rc::new(RunningAppState::new(
|
||||
servo,
|
||||
window.clone(),
|
||||
self.opts.headless,
|
||||
));
|
||||
running_state.new_toplevel_webview(self.initial_url.clone().into_url());
|
||||
|
||||
self.servo = Some(servo);
|
||||
if let Some(ref mut minibrowser) = self.minibrowser {
|
||||
minibrowser.update(window.winit_window().unwrap(), &running_state, "init");
|
||||
window.set_toolbar_height(minibrowser.toolbar_height);
|
||||
}
|
||||
|
||||
self.state = AppState::Running(running_state);
|
||||
}
|
||||
|
||||
pub fn is_animating(&self) -> bool {
|
||||
self.windows.iter().any(|(_, window)| window.is_animating())
|
||||
}
|
||||
|
||||
/// Spins the Servo event loop, and (for now) handles a few other tasks:
|
||||
/// - Notifying Servo about incoming gamepad events
|
||||
/// - Receiving updates from Servo
|
||||
/// - Performing updates in the compositor, such as queued pinch zoom events
|
||||
///
|
||||
/// In the future, these tasks may be decoupled.
|
||||
fn handle_events(&mut self) -> PumpResult {
|
||||
// If the Gamepad API is enabled, handle gamepad events from GilRs.
|
||||
// Checking for focused_webview_id should ensure we'll have a valid browsing context.
|
||||
if pref!(dom_gamepad_enabled) && self.webviews.focused_webview_id().is_some() {
|
||||
self.webviews.handle_gamepad_events();
|
||||
}
|
||||
|
||||
// Take any new embedder messages from Servo.
|
||||
let servo = self.servo.as_mut().expect("Servo should be running.");
|
||||
let mut embedder_messages = servo.get_events();
|
||||
let mut need_present = false;
|
||||
let mut need_update = false;
|
||||
loop {
|
||||
// Consume and handle those embedder messages.
|
||||
let servo_event_response = self.webviews.handle_servo_events(
|
||||
servo,
|
||||
&mut self.clipboard,
|
||||
&self.opts,
|
||||
embedder_messages,
|
||||
);
|
||||
need_present |= servo_event_response.need_present;
|
||||
need_update |= servo_event_response.need_update;
|
||||
|
||||
// Runs the compositor, and receives and collects embedder messages from various Servo components.
|
||||
servo.handle_events(vec![]);
|
||||
|
||||
if self.webviews.shutdown_requested() {
|
||||
return PumpResult::Shutdown;
|
||||
}
|
||||
|
||||
// Take any new embedder messages from Servo itself.
|
||||
embedder_messages = servo.get_events();
|
||||
if embedder_messages.is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let present = if need_present {
|
||||
Present::Deferred
|
||||
} else {
|
||||
Present::None
|
||||
};
|
||||
|
||||
PumpResult::Continue {
|
||||
update: need_update,
|
||||
present,
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle events with winit contexts
|
||||
pub fn handle_events_with_winit(
|
||||
&mut self,
|
||||
event_loop: &ActiveEventLoop,
|
||||
window: Rc<dyn WindowPortsMethods>,
|
||||
) {
|
||||
match self.handle_events() {
|
||||
let AppState::Running(state) = &self.state else {
|
||||
return;
|
||||
};
|
||||
|
||||
match state.pump_event_loop() {
|
||||
PumpResult::Shutdown => {
|
||||
event_loop.exit();
|
||||
self.servo.take().unwrap().deinit();
|
||||
if let Some(ref mut minibrowser) = self.minibrowser {
|
||||
minibrowser.context.destroy();
|
||||
}
|
||||
state.shutdown();
|
||||
self.state = AppState::ShuttingDown;
|
||||
},
|
||||
PumpResult::Continue { update, present } => {
|
||||
if update {
|
||||
if let Some(ref mut minibrowser) = self.minibrowser {
|
||||
if minibrowser.update_webview_data(&mut self.webviews) {
|
||||
if minibrowser.update_webview_data(state) {
|
||||
// Update the minibrowser immediately. While we could update by requesting a
|
||||
// redraw, doing so would delay the location update by two frames.
|
||||
minibrowser.update(
|
||||
window.winit_window().unwrap(),
|
||||
&mut self.webviews,
|
||||
self.servo.as_ref(),
|
||||
state,
|
||||
"update_location_in_toolbar",
|
||||
);
|
||||
}
|
||||
|
@ -327,16 +264,21 @@ impl App {
|
|||
if let Some(window) = window.winit_window() {
|
||||
window.request_redraw();
|
||||
} else {
|
||||
self.servo.as_mut().unwrap().present();
|
||||
state.servo().present();
|
||||
}
|
||||
},
|
||||
Present::None => {},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
if matches!(self.state, AppState::ShuttingDown) {
|
||||
event_loop.exit();
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle all servo events with headless mode. Return true if servo request to shutdown.
|
||||
/// Handle all servo events with headless mode. Return true if the application should
|
||||
/// continue.
|
||||
pub fn handle_events_with_headless(&mut self) -> bool {
|
||||
let now = Instant::now();
|
||||
let event = winit::event::Event::UserEvent(WakerEvent);
|
||||
|
@ -347,20 +289,16 @@ impl App {
|
|||
now - self.t
|
||||
);
|
||||
self.t = now;
|
||||
// If self.servo is None here, it means that we're in the process of shutting down,
|
||||
// let's ignore events.
|
||||
if self.servo.is_none() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut exit = false;
|
||||
match self.handle_events() {
|
||||
// We should always be in the running state.
|
||||
let AppState::Running(state) = &self.state else {
|
||||
return false;
|
||||
};
|
||||
|
||||
match state.pump_event_loop() {
|
||||
PumpResult::Shutdown => {
|
||||
exit = true;
|
||||
self.servo.take().unwrap().deinit();
|
||||
if let Some(ref mut minibrowser) = self.minibrowser {
|
||||
minibrowser.context.destroy();
|
||||
}
|
||||
state.shutdown();
|
||||
self.state = AppState::ShuttingDown;
|
||||
},
|
||||
PumpResult::Continue { present, .. } => {
|
||||
match present {
|
||||
|
@ -368,13 +306,14 @@ impl App {
|
|||
// The compositor has painted to this frame.
|
||||
trace!("PumpResult::Present::Deferred");
|
||||
// In headless mode, we present directly.
|
||||
self.servo.as_mut().unwrap().present();
|
||||
state.servo().present();
|
||||
},
|
||||
Present::None => {},
|
||||
}
|
||||
},
|
||||
}
|
||||
exit
|
||||
|
||||
!matches!(self.state, AppState::ShuttingDown)
|
||||
}
|
||||
|
||||
/// Takes any events generated during `egui` updates and performs their actions.
|
||||
|
@ -382,7 +321,8 @@ impl App {
|
|||
let Some(minibrowser) = self.minibrowser.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let Some(servo) = self.servo.as_ref() else {
|
||||
// We should always be in the running state.
|
||||
let AppState::Running(state) = &self.state else {
|
||||
return;
|
||||
};
|
||||
|
||||
|
@ -397,34 +337,33 @@ impl App {
|
|||
warn!("failed to parse location");
|
||||
break;
|
||||
};
|
||||
if let Some(focused_webview) = self.webviews.focused_webview() {
|
||||
focused_webview.servo_webview.load(url.into_url());
|
||||
if let Some(focused_webview) = state.focused_webview() {
|
||||
focused_webview.load(url.into_url());
|
||||
}
|
||||
},
|
||||
MinibrowserEvent::Back => {
|
||||
if let Some(focused_webview) = self.webviews.focused_webview() {
|
||||
focused_webview.servo_webview.go_back(1);
|
||||
if let Some(focused_webview) = state.focused_webview() {
|
||||
focused_webview.go_back(1);
|
||||
}
|
||||
},
|
||||
MinibrowserEvent::Forward => {
|
||||
if let Some(focused_webview) = self.webviews.focused_webview() {
|
||||
focused_webview.servo_webview.go_forward(1);
|
||||
if let Some(focused_webview) = state.focused_webview() {
|
||||
focused_webview.go_forward(1);
|
||||
}
|
||||
},
|
||||
MinibrowserEvent::Reload => {
|
||||
minibrowser.update_location_dirty(false);
|
||||
if let Some(focused_webview) = self.webviews.focused_webview() {
|
||||
focused_webview.servo_webview.reload();
|
||||
if let Some(focused_webview) = state.focused_webview() {
|
||||
focused_webview.reload();
|
||||
}
|
||||
},
|
||||
MinibrowserEvent::NewWebView => {
|
||||
minibrowser.update_location_dirty(false);
|
||||
let webview = servo.new_webview(Url::parse("servo:newtab").unwrap());
|
||||
self.webviews.add(webview);
|
||||
state.new_toplevel_webview(Url::parse("servo:newtab").unwrap());
|
||||
},
|
||||
MinibrowserEvent::CloseWebView(id) => {
|
||||
minibrowser.update_location_dirty(false);
|
||||
self.webviews.close_webview(servo, id);
|
||||
state.close_webview(id);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -450,17 +389,16 @@ impl ApplicationHandler<WakerEvent> for App {
|
|||
now - self.t
|
||||
);
|
||||
self.t = now;
|
||||
// If self.servo is None here, it means that we're in the process of shutting down,
|
||||
// let's ignore events.
|
||||
let Some(ref mut servo) = self.servo else {
|
||||
|
||||
let AppState::Running(state) = &self.state else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(window) = self.windows.get(&window_id) else {
|
||||
return;
|
||||
};
|
||||
let window = window.clone();
|
||||
|
||||
let window = window.clone();
|
||||
if event == winit::event::WindowEvent::RedrawRequested {
|
||||
// We need to redraw the window for some reason.
|
||||
trace!("RedrawRequested");
|
||||
|
@ -468,16 +406,11 @@ impl ApplicationHandler<WakerEvent> for App {
|
|||
// WARNING: do not defer painting or presenting to some later tick of the event
|
||||
// loop or servoshell may become unresponsive! (servo#30312)
|
||||
if let Some(ref mut minibrowser) = self.minibrowser {
|
||||
minibrowser.update(
|
||||
window.winit_window().unwrap(),
|
||||
&mut self.webviews,
|
||||
Some(servo),
|
||||
"RedrawRequested",
|
||||
);
|
||||
minibrowser.update(window.winit_window().unwrap(), state, "RedrawRequested");
|
||||
minibrowser.paint(window.winit_window().unwrap());
|
||||
}
|
||||
|
||||
servo.present();
|
||||
state.servo().present();
|
||||
}
|
||||
|
||||
// Handle the event
|
||||
|
@ -512,8 +445,7 @@ impl ApplicationHandler<WakerEvent> for App {
|
|||
if let WindowEvent::Resized(_) = event {
|
||||
minibrowser.update(
|
||||
window.winit_window().unwrap(),
|
||||
&mut self.webviews,
|
||||
Some(servo),
|
||||
state,
|
||||
"Sync WebView size with Window Resize event",
|
||||
);
|
||||
}
|
||||
|
@ -530,7 +462,7 @@ impl ApplicationHandler<WakerEvent> for App {
|
|||
}
|
||||
}
|
||||
if !consumed {
|
||||
window.handle_winit_event(servo, &mut self.clipboard, &mut self.webviews, event);
|
||||
window.handle_winit_event(state.clone(), event);
|
||||
}
|
||||
|
||||
let animating = self.is_animating();
|
||||
|
@ -558,12 +490,10 @@ impl ApplicationHandler<WakerEvent> for App {
|
|||
now - self.t
|
||||
);
|
||||
self.t = now;
|
||||
// If self.servo is None here, it means that we're in the process of shutting down,
|
||||
// let's ignore events.
|
||||
if self.servo.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
if !matches!(self.state, AppState::Running(_)) {
|
||||
return;
|
||||
};
|
||||
let Some(window) = self.windows.values().next() else {
|
||||
return;
|
||||
};
|
||||
|
|
686
ports/servoshell/desktop/app_state.rs
Normal file
686
ports/servoshell/desktop/app_state.rs
Normal file
|
@ -0,0 +1,686 @@
|
|||
/* 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::{Ref, RefCell, RefMut};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
use std::thread;
|
||||
|
||||
use arboard::Clipboard;
|
||||
use euclid::Vector2D;
|
||||
use keyboard_types::{Key, KeyboardEvent, Modifiers, ShortcutMatcher};
|
||||
use log::{error, info, warn};
|
||||
use servo::base::id::WebViewId;
|
||||
use servo::config::pref;
|
||||
use servo::ipc_channel::ipc::IpcSender;
|
||||
use servo::webrender_api::units::{DeviceIntPoint, DeviceIntSize};
|
||||
use servo::webrender_api::ScrollLocation;
|
||||
use servo::{
|
||||
AllowOrDenyRequest, CompositorEventVariant, FilterPattern, GamepadHapticEffectType, LoadStatus,
|
||||
PermissionPrompt, PermissionRequest, PromptCredentialsInput, PromptDefinition, PromptOrigin,
|
||||
PromptResult, Servo, ServoDelegate, ServoError, TouchEventType, WebView, WebViewDelegate,
|
||||
};
|
||||
use tinyfiledialogs::{self, MessageBoxIcon, OkCancel};
|
||||
use url::Url;
|
||||
|
||||
use super::app::{Present, PumpResult};
|
||||
use super::dialog::Dialog;
|
||||
use super::gamepad::GamepadSupport;
|
||||
use super::keyutils::CMD_OR_CONTROL;
|
||||
use super::window_trait::{WindowPortsMethods, LINE_HEIGHT};
|
||||
|
||||
pub(crate) enum AppState {
|
||||
Initializing,
|
||||
Running(Rc<RunningAppState>),
|
||||
ShuttingDown,
|
||||
}
|
||||
|
||||
pub(crate) struct RunningAppState {
|
||||
/// A handle to the Servo instance of the [`RunningAppState`]. This is not stored inside
|
||||
/// `inner` so that we can keep a reference to Servo in order to spin the event loop,
|
||||
/// which will in turn call delegates doing a mutable borrow on `inner`.
|
||||
servo: Servo,
|
||||
inner: RefCell<RunningAppStateInner>,
|
||||
}
|
||||
|
||||
pub struct RunningAppStateInner {
|
||||
/// Whether or not this is a headless servoshell window.
|
||||
headless: bool,
|
||||
|
||||
/// The clipboard to use for this collection of [`WebView`]s.
|
||||
pub(crate) clipboard: Option<Clipboard>,
|
||||
|
||||
/// List of top-level browsing contexts.
|
||||
/// Modified by EmbedderMsg::WebViewOpened and EmbedderMsg::WebViewClosed,
|
||||
/// and we exit if it ever becomes empty.
|
||||
webviews: HashMap<WebViewId, WebView>,
|
||||
|
||||
/// The order in which the webviews were created.
|
||||
creation_order: Vec<WebViewId>,
|
||||
|
||||
/// The webview that is currently focused.
|
||||
/// Modified by EmbedderMsg::WebViewFocused and EmbedderMsg::WebViewBlurred.
|
||||
focused_webview_id: Option<WebViewId>,
|
||||
|
||||
/// The current set of open dialogs.
|
||||
dialogs: HashMap<WebViewId, Vec<Dialog>>,
|
||||
|
||||
/// A handle to the Window that Servo is rendering in -- either headed or headless.
|
||||
window: Rc<dyn WindowPortsMethods>,
|
||||
|
||||
/// Gamepad support, which may be `None` if it failed to initialize.
|
||||
gamepad_support: Option<GamepadSupport>,
|
||||
|
||||
/// Whether or not the application interface needs to be updated.
|
||||
need_update: bool,
|
||||
|
||||
/// Whether or not the application needs to be redrawn.
|
||||
need_present: bool,
|
||||
}
|
||||
|
||||
impl Drop for RunningAppState {
|
||||
fn drop(&mut self) {
|
||||
self.servo.deinit();
|
||||
}
|
||||
}
|
||||
|
||||
impl RunningAppState {
|
||||
pub fn new(
|
||||
servo: Servo,
|
||||
window: Rc<dyn WindowPortsMethods>,
|
||||
headless: bool,
|
||||
) -> RunningAppState {
|
||||
RunningAppState {
|
||||
servo,
|
||||
inner: RefCell::new(RunningAppStateInner {
|
||||
headless,
|
||||
clipboard: Clipboard::new().ok(),
|
||||
webviews: HashMap::default(),
|
||||
creation_order: Default::default(),
|
||||
focused_webview_id: None,
|
||||
dialogs: Default::default(),
|
||||
window,
|
||||
gamepad_support: GamepadSupport::maybe_new(),
|
||||
need_update: false,
|
||||
need_present: false,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_toplevel_webview(self: &Rc<Self>, url: Url) {
|
||||
let webview = self.servo().new_webview(url);
|
||||
webview.set_delegate(self.clone());
|
||||
self.add(webview);
|
||||
}
|
||||
|
||||
pub(crate) fn inner(&self) -> Ref<RunningAppStateInner> {
|
||||
self.inner.borrow()
|
||||
}
|
||||
|
||||
pub(crate) fn inner_mut(&self) -> RefMut<RunningAppStateInner> {
|
||||
self.inner.borrow_mut()
|
||||
}
|
||||
|
||||
pub(crate) fn servo(&self) -> &Servo {
|
||||
&self.servo
|
||||
}
|
||||
|
||||
/// Spins the internal application event loop.
|
||||
///
|
||||
/// - Notifies Servo about incoming gamepad events
|
||||
/// - Spin the Servo event loop, which will run the compositor and trigger delegate methods.
|
||||
pub(crate) fn pump_event_loop(&self) -> PumpResult {
|
||||
if pref!(dom_gamepad_enabled) {
|
||||
self.handle_gamepad_events();
|
||||
}
|
||||
|
||||
let should_continue = self.servo().spin_event_loop();
|
||||
|
||||
// Delegate handlers may have asked us to present or update compositor contents.
|
||||
let need_present = std::mem::replace(&mut self.inner_mut().need_present, false);
|
||||
let need_update = std::mem::replace(&mut self.inner_mut().need_update, false);
|
||||
|
||||
if !should_continue {
|
||||
return PumpResult::Shutdown;
|
||||
}
|
||||
|
||||
// Currently, egui-file-dialog dialogs need to be constantly presented or animations aren't fluid.
|
||||
let need_present = need_present || self.has_active_file_dialog();
|
||||
|
||||
let present = if need_present {
|
||||
Present::Deferred
|
||||
} else {
|
||||
Present::None
|
||||
};
|
||||
|
||||
PumpResult::Continue {
|
||||
update: need_update,
|
||||
present,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn add(&self, webview: WebView) {
|
||||
self.inner_mut().creation_order.push(webview.id());
|
||||
self.inner_mut().webviews.insert(webview.id(), webview);
|
||||
}
|
||||
|
||||
pub(crate) fn shutdown(&self) {
|
||||
self.inner_mut().webviews.clear();
|
||||
}
|
||||
|
||||
pub(crate) fn for_each_active_dialog(&self, callback: impl Fn(&mut Dialog) -> bool) {
|
||||
let Some(webview_id) = self.focused_webview().as_ref().map(WebView::id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(dialogs) = self.inner_mut().dialogs.get_mut(&webview_id) {
|
||||
dialogs.retain_mut(callback);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close_webview(&self, webview_id: WebViewId) {
|
||||
// This can happen because we can trigger a close with a UI action and then get the
|
||||
// close event from Servo later.
|
||||
let mut inner = self.inner_mut();
|
||||
if !inner.webviews.contains_key(&webview_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
inner.webviews.retain(|&id, _| id != webview_id);
|
||||
inner.creation_order.retain(|&id| id != webview_id);
|
||||
inner.focused_webview_id = None;
|
||||
inner.dialogs.remove(&webview_id);
|
||||
|
||||
let last_created = inner
|
||||
.creation_order
|
||||
.last()
|
||||
.and_then(|id| inner.webviews.get(id));
|
||||
|
||||
match last_created {
|
||||
Some(last_created_webview) => last_created_webview.focus(),
|
||||
None => self.servo.start_shutting_down(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn focused_webview(&self) -> Option<WebView> {
|
||||
self.inner()
|
||||
.focused_webview_id
|
||||
.and_then(|id| self.inner().webviews.get(&id).cloned())
|
||||
}
|
||||
|
||||
// Returns the webviews in the creation order.
|
||||
pub fn webviews(&self) -> Vec<(WebViewId, WebView)> {
|
||||
let inner = self.inner();
|
||||
inner
|
||||
.creation_order
|
||||
.iter()
|
||||
.map(|id| (*id, inner.webviews.get(id).unwrap().clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn handle_gamepad_events(&self) {
|
||||
let Some(active_webview) = self.focused_webview() else {
|
||||
return;
|
||||
};
|
||||
if let Some(gamepad_support) = self.inner_mut().gamepad_support.as_mut() {
|
||||
gamepad_support.handle_gamepad_events(active_webview);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn focus_webview_by_index(&self, index: usize) {
|
||||
if let Some((_, webview)) = self.webviews().get(index) {
|
||||
webview.focus();
|
||||
}
|
||||
}
|
||||
|
||||
fn has_active_file_dialog(&self) -> bool {
|
||||
let Some(webview) = self.focused_webview() else {
|
||||
return false;
|
||||
};
|
||||
let inner = self.inner();
|
||||
let Some(dialogs) = inner.dialogs.get(&webview.id()) else {
|
||||
return false;
|
||||
};
|
||||
dialogs.iter().any(Dialog::is_file_dialog)
|
||||
}
|
||||
|
||||
pub(crate) fn get_focused_webview_index(&self) -> Option<usize> {
|
||||
let focused_id = self.inner().focused_webview_id?;
|
||||
self.webviews()
|
||||
.iter()
|
||||
.position(|webview| webview.0 == focused_id)
|
||||
}
|
||||
|
||||
/// Handle servoshell key bindings that may have been prevented by the page in the focused webview.
|
||||
fn handle_overridable_key_bindings(&self, webview: ::servo::WebView, event: KeyboardEvent) {
|
||||
let origin = webview.rect().min.ceil().to_i32();
|
||||
ShortcutMatcher::from_event(event)
|
||||
.shortcut(CMD_OR_CONTROL, '=', || {
|
||||
webview.set_zoom(1.1);
|
||||
})
|
||||
.shortcut(CMD_OR_CONTROL, '+', || {
|
||||
webview.set_zoom(1.1);
|
||||
})
|
||||
.shortcut(CMD_OR_CONTROL, '-', || {
|
||||
webview.set_zoom(1.0 / 1.1);
|
||||
})
|
||||
.shortcut(CMD_OR_CONTROL, '0', || {
|
||||
webview.reset_zoom();
|
||||
})
|
||||
.shortcut(Modifiers::empty(), Key::PageDown, || {
|
||||
let scroll_location = ScrollLocation::Delta(Vector2D::new(
|
||||
0.0,
|
||||
-self.inner().window.page_height() + 2.0 * LINE_HEIGHT,
|
||||
));
|
||||
webview.notify_scroll_event(scroll_location, origin, TouchEventType::Move);
|
||||
})
|
||||
.shortcut(Modifiers::empty(), Key::PageUp, || {
|
||||
let scroll_location = ScrollLocation::Delta(Vector2D::new(
|
||||
0.0,
|
||||
self.inner().window.page_height() - 2.0 * LINE_HEIGHT,
|
||||
));
|
||||
webview.notify_scroll_event(scroll_location, origin, TouchEventType::Move);
|
||||
})
|
||||
.shortcut(Modifiers::empty(), Key::Home, || {
|
||||
webview.notify_scroll_event(ScrollLocation::Start, origin, TouchEventType::Move);
|
||||
})
|
||||
.shortcut(Modifiers::empty(), Key::End, || {
|
||||
webview.notify_scroll_event(ScrollLocation::End, origin, TouchEventType::Move);
|
||||
})
|
||||
.shortcut(Modifiers::empty(), Key::ArrowUp, || {
|
||||
let location = ScrollLocation::Delta(Vector2D::new(0.0, 3.0 * LINE_HEIGHT));
|
||||
webview.notify_scroll_event(location, origin, TouchEventType::Move);
|
||||
})
|
||||
.shortcut(Modifiers::empty(), Key::ArrowDown, || {
|
||||
let location = ScrollLocation::Delta(Vector2D::new(0.0, -3.0 * LINE_HEIGHT));
|
||||
webview.notify_scroll_event(location, origin, TouchEventType::Move);
|
||||
})
|
||||
.shortcut(Modifiers::empty(), Key::ArrowLeft, || {
|
||||
let location = ScrollLocation::Delta(Vector2D::new(LINE_HEIGHT, 0.0));
|
||||
webview.notify_scroll_event(location, origin, TouchEventType::Move);
|
||||
})
|
||||
.shortcut(Modifiers::empty(), Key::ArrowRight, || {
|
||||
let location = ScrollLocation::Delta(Vector2D::new(-LINE_HEIGHT, 0.0));
|
||||
webview.notify_scroll_event(location, origin, TouchEventType::Move);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl ServoDelegate for RunningAppState {
|
||||
fn notify_devtools_server_started(&self, _servo: &Servo, port: u16, _token: String) {
|
||||
info!("Devtools Server running on port {port}");
|
||||
}
|
||||
|
||||
fn request_devtools_connection(&self, _servo: &Servo, request: AllowOrDenyRequest) {
|
||||
request.allow();
|
||||
}
|
||||
|
||||
fn notify_error(&self, _servo: &Servo, error: ServoError) {
|
||||
error!("Saw Servo error: {error:?}!");
|
||||
}
|
||||
}
|
||||
|
||||
impl WebViewDelegate for RunningAppState {
|
||||
fn notify_status_text_changed(&self, _webview: servo::WebView, _status: Option<String>) {
|
||||
self.inner_mut().need_update = true;
|
||||
}
|
||||
|
||||
fn notify_page_title_changed(&self, webview: servo::WebView, title: Option<String>) {
|
||||
if webview.focused() {
|
||||
let window_title = format!("{} - Servo", title.clone().unwrap_or_default());
|
||||
self.inner().window.set_title(&window_title);
|
||||
self.inner_mut().need_update = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn request_move_to(&self, _: servo::WebView, new_position: DeviceIntPoint) {
|
||||
self.inner().window.set_position(new_position);
|
||||
}
|
||||
|
||||
fn request_resize_to(&self, webview: servo::WebView, new_size: DeviceIntSize) {
|
||||
let mut rect = webview.rect();
|
||||
rect.set_size(new_size.to_f32());
|
||||
webview.move_resize(rect);
|
||||
self.inner().window.request_resize(&webview, new_size);
|
||||
}
|
||||
|
||||
fn show_prompt(
|
||||
&self,
|
||||
webview: servo::WebView,
|
||||
definition: PromptDefinition,
|
||||
origin: PromptOrigin,
|
||||
) {
|
||||
let res = if self.inner().headless {
|
||||
match definition {
|
||||
PromptDefinition::Alert(_message, sender) => sender.send(()),
|
||||
PromptDefinition::OkCancel(_message, sender) => sender.send(PromptResult::Primary),
|
||||
PromptDefinition::Input(_message, default, sender) => {
|
||||
sender.send(Some(default.to_owned()))
|
||||
},
|
||||
PromptDefinition::Credentials(sender) => sender.send(PromptCredentialsInput {
|
||||
username: None,
|
||||
password: None,
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
thread::Builder::new()
|
||||
.name("AlertDialog".to_owned())
|
||||
.spawn(move || match definition {
|
||||
PromptDefinition::Alert(mut message, sender) => {
|
||||
if origin == PromptOrigin::Untrusted {
|
||||
message = tiny_dialog_escape(&message);
|
||||
}
|
||||
tinyfiledialogs::message_box_ok(
|
||||
"Alert!",
|
||||
&message,
|
||||
MessageBoxIcon::Warning,
|
||||
);
|
||||
sender.send(())
|
||||
},
|
||||
PromptDefinition::OkCancel(mut message, sender) => {
|
||||
if origin == PromptOrigin::Untrusted {
|
||||
message = tiny_dialog_escape(&message);
|
||||
}
|
||||
let result = tinyfiledialogs::message_box_ok_cancel(
|
||||
"",
|
||||
&message,
|
||||
MessageBoxIcon::Warning,
|
||||
OkCancel::Cancel,
|
||||
);
|
||||
sender.send(match result {
|
||||
OkCancel::Ok => PromptResult::Primary,
|
||||
OkCancel::Cancel => PromptResult::Secondary,
|
||||
})
|
||||
},
|
||||
PromptDefinition::Input(mut message, mut default, sender) => {
|
||||
if origin == PromptOrigin::Untrusted {
|
||||
message = tiny_dialog_escape(&message);
|
||||
default = tiny_dialog_escape(&default);
|
||||
}
|
||||
let result = tinyfiledialogs::input_box("", &message, &default);
|
||||
sender.send(result)
|
||||
},
|
||||
PromptDefinition::Credentials(sender) => {
|
||||
// TODO: figure out how to make the message a localized string
|
||||
let username = tinyfiledialogs::input_box("", "username", "");
|
||||
let password = tinyfiledialogs::input_box("", "password", "");
|
||||
sender.send(PromptCredentialsInput { username, password })
|
||||
},
|
||||
})
|
||||
.unwrap()
|
||||
.join()
|
||||
.expect("Thread spawning failed")
|
||||
};
|
||||
|
||||
if let Err(e) = res {
|
||||
webview.send_error(format!("Failed to send Prompt response: {e}"))
|
||||
}
|
||||
}
|
||||
|
||||
fn request_open_auxiliary_webview(
|
||||
&self,
|
||||
parent_webview: servo::WebView,
|
||||
) -> Option<servo::WebView> {
|
||||
let webview = self.servo.new_auxiliary_webview();
|
||||
webview.set_delegate(parent_webview.delegate());
|
||||
self.add(webview.clone());
|
||||
Some(webview)
|
||||
}
|
||||
|
||||
fn notify_ready_to_show(&self, webview: servo::WebView) {
|
||||
let scale = self.inner().window.hidpi_factor().get();
|
||||
let toolbar = self.inner().window.toolbar_height().get();
|
||||
|
||||
// Adjust for our toolbar height.
|
||||
// TODO: Adjust for egui window decorations if we end up using those
|
||||
let mut rect = self
|
||||
.inner()
|
||||
.window
|
||||
.get_coordinates()
|
||||
.get_viewport()
|
||||
.to_f32();
|
||||
rect.min.y += toolbar * scale;
|
||||
|
||||
webview.focus();
|
||||
webview.move_resize(rect);
|
||||
webview.raise_to_top(true);
|
||||
}
|
||||
|
||||
fn notify_closed(&self, webview: servo::WebView) {
|
||||
self.close_webview(webview.id());
|
||||
}
|
||||
|
||||
fn notify_focus_changed(&self, webview: servo::WebView, focused: bool) {
|
||||
let mut inner_mut = self.inner_mut();
|
||||
if focused {
|
||||
webview.show(true);
|
||||
inner_mut.need_update = true;
|
||||
inner_mut.focused_webview_id = Some(webview.id());
|
||||
} else if inner_mut.focused_webview_id == Some(webview.id()) {
|
||||
inner_mut.focused_webview_id = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn notify_keyboard_event(&self, webview: servo::WebView, keyboard_event: KeyboardEvent) {
|
||||
self.handle_overridable_key_bindings(webview, keyboard_event);
|
||||
}
|
||||
|
||||
fn clear_clipboard_contents(&self, _webview: servo::WebView) {
|
||||
self.inner_mut()
|
||||
.clipboard
|
||||
.as_mut()
|
||||
.and_then(|clipboard| clipboard.clear().ok());
|
||||
}
|
||||
|
||||
fn get_clipboard_contents(&self, _webview: servo::WebView, result_sender: IpcSender<String>) {
|
||||
let contents = self
|
||||
.inner_mut()
|
||||
.clipboard
|
||||
.as_mut()
|
||||
.and_then(|clipboard| clipboard.get_text().ok())
|
||||
.unwrap_or_default();
|
||||
if let Err(e) = result_sender.send(contents) {
|
||||
warn!("Failed to send clipboard ({})", e);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_clipboard_contents(&self, _webview: servo::WebView, text: String) {
|
||||
if let Some(clipboard) = self.inner_mut().clipboard.as_mut() {
|
||||
if let Err(e) = clipboard.set_text(text) {
|
||||
warn!("Error setting clipboard contents ({})", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn notify_cursor_changed(&self, _webview: servo::WebView, cursor: servo::Cursor) {
|
||||
self.inner().window.set_cursor(cursor);
|
||||
}
|
||||
|
||||
fn notify_load_status_changed(&self, _webview: servo::WebView, _status: LoadStatus) {
|
||||
self.inner_mut().need_update = true;
|
||||
}
|
||||
|
||||
fn request_fullscreen_state_change(&self, _webview: servo::WebView, fullscreen_state: bool) {
|
||||
self.inner().window.set_fullscreen(fullscreen_state);
|
||||
}
|
||||
|
||||
fn show_bluetooth_device_dialog(
|
||||
&self,
|
||||
webview: servo::WebView,
|
||||
devices: Vec<String>,
|
||||
response_sender: IpcSender<Option<String>>,
|
||||
) {
|
||||
let selected = platform_get_selected_devices(devices);
|
||||
if let Err(e) = response_sender.send(selected) {
|
||||
webview.send_error(format!(
|
||||
"Failed to send GetSelectedBluetoothDevice response: {e}"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn show_file_selection_dialog(
|
||||
&self,
|
||||
webview: servo::WebView,
|
||||
filter_pattern: Vec<FilterPattern>,
|
||||
allow_select_mutiple: bool,
|
||||
response_sender: IpcSender<Option<Vec<PathBuf>>>,
|
||||
) {
|
||||
let mut inner_mut = self.inner_mut();
|
||||
inner_mut
|
||||
.dialogs
|
||||
.entry(webview.id())
|
||||
.or_default()
|
||||
.push(Dialog::new_file_dialog(
|
||||
allow_select_mutiple,
|
||||
response_sender,
|
||||
filter_pattern,
|
||||
));
|
||||
inner_mut.need_update = true;
|
||||
inner_mut.need_present = true;
|
||||
}
|
||||
|
||||
fn request_permission(
|
||||
&self,
|
||||
_webview: servo::WebView,
|
||||
prompt: PermissionPrompt,
|
||||
result_sender: IpcSender<PermissionRequest>,
|
||||
) {
|
||||
let _ = result_sender.send(match self.inner().headless {
|
||||
true => PermissionRequest::Denied,
|
||||
false => prompt_user(prompt),
|
||||
});
|
||||
}
|
||||
|
||||
fn notify_new_frame_ready(&self, _webview: servo::WebView) {
|
||||
self.inner_mut().need_present = true;
|
||||
}
|
||||
|
||||
fn notify_event_delivered(&self, webview: servo::WebView, event: CompositorEventVariant) {
|
||||
if let CompositorEventVariant::MouseButtonEvent = event {
|
||||
webview.raise_to_top(true);
|
||||
webview.focus();
|
||||
}
|
||||
}
|
||||
|
||||
fn play_gamepad_haptic_effect(
|
||||
&self,
|
||||
_webview: servo::WebView,
|
||||
index: usize,
|
||||
effect_type: GamepadHapticEffectType,
|
||||
effect_complete_sender: IpcSender<bool>,
|
||||
) {
|
||||
match self.inner_mut().gamepad_support.as_mut() {
|
||||
Some(gamepad_support) => {
|
||||
gamepad_support.play_haptic_effect(index, effect_type, effect_complete_sender);
|
||||
},
|
||||
None => {
|
||||
let _ = effect_complete_sender.send(false);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn stop_gamepad_haptic_effect(
|
||||
&self,
|
||||
_webview: servo::WebView,
|
||||
index: usize,
|
||||
haptic_stop_sender: IpcSender<bool>,
|
||||
) {
|
||||
let stopped = match self.inner_mut().gamepad_support.as_mut() {
|
||||
Some(gamepad_support) => gamepad_support.stop_haptic_effect(index),
|
||||
None => false,
|
||||
};
|
||||
let _ = haptic_stop_sender.send(stopped);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn prompt_user(prompt: PermissionPrompt) -> PermissionRequest {
|
||||
use tinyfiledialogs::YesNo;
|
||||
|
||||
let message = match prompt {
|
||||
PermissionPrompt::Request(permission_name) => {
|
||||
format!("Do you want to grant permission for {:?}?", permission_name)
|
||||
},
|
||||
PermissionPrompt::Insecure(permission_name) => {
|
||||
format!(
|
||||
"The {:?} feature is only safe to use in secure context, but servo can't guarantee\n\
|
||||
that the current context is secure. Do you want to proceed and grant permission?",
|
||||
permission_name
|
||||
)
|
||||
},
|
||||
};
|
||||
|
||||
match tinyfiledialogs::message_box_yes_no(
|
||||
"Permission request dialog",
|
||||
&message,
|
||||
MessageBoxIcon::Question,
|
||||
YesNo::No,
|
||||
) {
|
||||
YesNo::Yes => PermissionRequest::Granted,
|
||||
YesNo::No => PermissionRequest::Denied,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn prompt_user(_prompt: PermissionPrompt) -> PermissionRequest {
|
||||
// TODO popup only supported on linux
|
||||
PermissionRequest::Denied
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn platform_get_selected_devices(devices: Vec<String>) -> Option<String> {
|
||||
thread::Builder::new()
|
||||
.name("DevicePicker".to_owned())
|
||||
.spawn(move || {
|
||||
let dialog_rows: Vec<&str> = devices.iter().map(|s| s.as_ref()).collect();
|
||||
let dialog_rows: Option<&[&str]> = Some(dialog_rows.as_slice());
|
||||
|
||||
match tinyfiledialogs::list_dialog("Choose a device", &["Id", "Name"], dialog_rows) {
|
||||
Some(device) => {
|
||||
// The device string format will be "Address|Name". We need the first part of it.
|
||||
device.split('|').next().map(|s| s.to_string())
|
||||
},
|
||||
None => None,
|
||||
}
|
||||
})
|
||||
.unwrap()
|
||||
.join()
|
||||
.expect("Thread spawning failed")
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn platform_get_selected_devices(devices: Vec<String>) -> Option<String> {
|
||||
for device in devices {
|
||||
if let Some(address) = device.split('|').next().map(|s| s.to_string()) {
|
||||
return Some(address);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
// This is a mitigation for #25498, not a verified solution.
|
||||
// There may be codepaths in tinyfiledialog.c that this is
|
||||
// inadquate against, as it passes the string via shell to
|
||||
// different programs depending on what the user has installed.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn tiny_dialog_escape(raw: &str) -> String {
|
||||
let s: String = raw
|
||||
.chars()
|
||||
.filter_map(|c| match c {
|
||||
'\n' => Some('\n'),
|
||||
'\0'..='\x1f' => None,
|
||||
'<' => Some('\u{FF1C}'),
|
||||
'>' => Some('\u{FF1E}'),
|
||||
'&' => Some('\u{FF06}'),
|
||||
_ => Some(c),
|
||||
})
|
||||
.collect();
|
||||
shellwords::escape(&s)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn tiny_dialog_escape(raw: &str) -> String {
|
||||
raw.to_string()
|
||||
}
|
|
@ -31,9 +31,10 @@ pub fn main() {
|
|||
let event_loop = EventsLoop::new(opts.headless, opts.output_file.is_some())
|
||||
.expect("Failed to create events loop");
|
||||
|
||||
let mut app = App::new(opts, preferences, servoshell_preferences, &event_loop);
|
||||
|
||||
event_loop.run_app(&mut app);
|
||||
{
|
||||
let mut app = App::new(opts, preferences, servoshell_preferences, &event_loop);
|
||||
event_loop.run_app(&mut app);
|
||||
}
|
||||
|
||||
crate::platform::deinit(clean_shutdown)
|
||||
}
|
||||
|
|
|
@ -92,4 +92,8 @@ impl Dialog {
|
|||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_file_dialog(&self) -> bool {
|
||||
matches!(self, Dialog::File(..))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -93,7 +93,7 @@ impl EventsLoop {
|
|||
app.init(None);
|
||||
loop {
|
||||
self.sleep(flag, condvar);
|
||||
if app.handle_events_with_headless() {
|
||||
if !app.handle_events_with_headless() {
|
||||
break;
|
||||
}
|
||||
if !app.is_animating() {
|
||||
|
|
230
ports/servoshell/desktop/gamepad.rs
Normal file
230
ports/servoshell/desktop/gamepad.rs
Normal file
|
@ -0,0 +1,230 @@
|
|||
/* 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::collections::HashMap;
|
||||
|
||||
use gilrs::ff::{BaseEffect, BaseEffectType, Effect, EffectBuilder, Repeat, Replay, Ticks};
|
||||
use gilrs::{EventType, Gilrs};
|
||||
use log::{debug, warn};
|
||||
use servo::ipc_channel::ipc::IpcSender;
|
||||
use servo::{
|
||||
GamepadEvent, GamepadHapticEffectType, GamepadIndex, GamepadInputBounds,
|
||||
GamepadSupportedHapticEffects, GamepadUpdateType, WebView,
|
||||
};
|
||||
|
||||
pub struct HapticEffect {
|
||||
pub effect: Effect,
|
||||
pub sender: IpcSender<bool>,
|
||||
}
|
||||
|
||||
pub(crate) struct GamepadSupport {
|
||||
handle: Gilrs,
|
||||
haptic_effects: HashMap<usize, HapticEffect>,
|
||||
}
|
||||
|
||||
impl GamepadSupport {
|
||||
pub(crate) fn maybe_new() -> Option<Self> {
|
||||
let handle = match Gilrs::new() {
|
||||
Ok(handle) => handle,
|
||||
Err(error) => {
|
||||
warn!("Error creating gamepad input connection ({error})");
|
||||
return None;
|
||||
},
|
||||
};
|
||||
Some(Self {
|
||||
handle,
|
||||
haptic_effects: 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);
|
||||
let name = gamepad.name();
|
||||
let index = GamepadIndex(event.id.into());
|
||||
let mut gamepad_event: Option<GamepadEvent> = None;
|
||||
match event.event {
|
||||
EventType::ButtonPressed(button, _) => {
|
||||
let mapped_index = Self::map_gamepad_button(button);
|
||||
// We only want to send this for a valid digital button, aka on/off only
|
||||
if !matches!(mapped_index, 6 | 7 | 17) {
|
||||
let update_type = GamepadUpdateType::Button(mapped_index, 1.0);
|
||||
gamepad_event = Some(GamepadEvent::Updated(index, update_type));
|
||||
}
|
||||
},
|
||||
EventType::ButtonReleased(button, _) => {
|
||||
let mapped_index = Self::map_gamepad_button(button);
|
||||
// We only want to send this for a valid digital button, aka on/off only
|
||||
if !matches!(mapped_index, 6 | 7 | 17) {
|
||||
let update_type = GamepadUpdateType::Button(mapped_index, 0.0);
|
||||
gamepad_event = Some(GamepadEvent::Updated(index, update_type));
|
||||
}
|
||||
},
|
||||
EventType::ButtonChanged(button, value, _) => {
|
||||
let mapped_index = Self::map_gamepad_button(button);
|
||||
// We only want to send this for a valid non-digital button, aka the triggers
|
||||
if matches!(mapped_index, 6 | 7) {
|
||||
let update_type = GamepadUpdateType::Button(mapped_index, value as f64);
|
||||
gamepad_event = Some(GamepadEvent::Updated(index, update_type));
|
||||
}
|
||||
},
|
||||
EventType::AxisChanged(axis, value, _) => {
|
||||
// Map axis index and value to represent Standard Gamepad axis
|
||||
// <https://www.w3.org/TR/gamepad/#dfn-represents-a-standard-gamepad-axis>
|
||||
let mapped_axis: usize = match axis {
|
||||
gilrs::Axis::LeftStickX => 0,
|
||||
gilrs::Axis::LeftStickY => 1,
|
||||
gilrs::Axis::RightStickX => 2,
|
||||
gilrs::Axis::RightStickY => 3,
|
||||
_ => 4, // Other axes do not map to "standard" gamepad mapping and are ignored
|
||||
};
|
||||
if mapped_axis < 4 {
|
||||
// The Gamepad spec designates down as positive and up as negative.
|
||||
// GilRs does the inverse of this, so correct for it here.
|
||||
let axis_value = match mapped_axis {
|
||||
0 | 2 => value,
|
||||
1 | 3 => -value,
|
||||
_ => 0., // Should not reach here
|
||||
};
|
||||
let update_type = GamepadUpdateType::Axis(mapped_axis, axis_value as f64);
|
||||
gamepad_event = Some(GamepadEvent::Updated(index, update_type));
|
||||
}
|
||||
},
|
||||
EventType::Connected => {
|
||||
let name = String::from(name);
|
||||
let bounds = GamepadInputBounds {
|
||||
axis_bounds: (-1.0, 1.0),
|
||||
button_bounds: (0.0, 1.0),
|
||||
};
|
||||
// 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());
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
|
||||
if let Some(event) = gamepad_event {
|
||||
active_webview.notify_gamepad_event(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Map button index and value to represent Standard Gamepad button
|
||||
// <https://www.w3.org/TR/gamepad/#dfn-represents-a-standard-gamepad-button>
|
||||
fn map_gamepad_button(button: gilrs::Button) -> usize {
|
||||
match button {
|
||||
gilrs::Button::South => 0,
|
||||
gilrs::Button::East => 1,
|
||||
gilrs::Button::West => 2,
|
||||
gilrs::Button::North => 3,
|
||||
gilrs::Button::LeftTrigger => 4,
|
||||
gilrs::Button::RightTrigger => 5,
|
||||
gilrs::Button::LeftTrigger2 => 6,
|
||||
gilrs::Button::RightTrigger2 => 7,
|
||||
gilrs::Button::Select => 8,
|
||||
gilrs::Button::Start => 9,
|
||||
gilrs::Button::LeftThumb => 10,
|
||||
gilrs::Button::RightThumb => 11,
|
||||
gilrs::Button::DPadUp => 12,
|
||||
gilrs::Button::DPadDown => 13,
|
||||
gilrs::Button::DPadLeft => 14,
|
||||
gilrs::Button::DPadRight => 15,
|
||||
gilrs::Button::Mode => 16,
|
||||
_ => 17, // Other buttons do not map to "standard" gamepad mapping and are ignored
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn play_haptic_effect(
|
||||
&mut self,
|
||||
index: usize,
|
||||
effect_type: GamepadHapticEffectType,
|
||||
effect_complete_sender: IpcSender<bool>,
|
||||
) {
|
||||
let GamepadHapticEffectType::DualRumble(params) = effect_type;
|
||||
if let Some(connected_gamepad) = self
|
||||
.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,
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) 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
|
||||
}
|
||||
}
|
|
@ -10,7 +10,6 @@ use std::env;
|
|||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use arboard::Clipboard;
|
||||
use euclid::{Angle, Length, Point2D, Rotation3D, Scale, Size2D, UnknownUnit, Vector2D, Vector3D};
|
||||
use keyboard_types::{Modifiers, ShortcutMatcher};
|
||||
use log::{debug, info};
|
||||
|
@ -26,7 +25,7 @@ use servo::webrender_api::ScrollLocation;
|
|||
use servo::webrender_traits::SurfmanRenderingContext;
|
||||
use servo::{
|
||||
ClipboardEventType, Cursor, Key, KeyState, KeyboardEvent, MouseButton as ServoMouseButton,
|
||||
Servo, Theme, TouchEventType, TouchId, WebView, WheelDelta, WheelMode,
|
||||
Theme, TouchEventType, TouchId, WebView, WheelDelta, WheelMode,
|
||||
};
|
||||
use surfman::{Context, Device, SurfaceType};
|
||||
use url::Url;
|
||||
|
@ -37,9 +36,9 @@ use winit::keyboard::{Key as LogicalKey, ModifiersState, NamedKey};
|
|||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
use winit::window::Icon;
|
||||
|
||||
use super::app_state::RunningAppState;
|
||||
use super::geometry::{winit_position_to_euclid_point, winit_size_to_euclid_size};
|
||||
use super::keyutils::{keyboard_event_from_winit, CMD_OR_ALT};
|
||||
use super::webview::{WebView as ServoShellWebView, WebViewManager};
|
||||
use super::window_trait::{WindowPortsMethods, LINE_HEIGHT};
|
||||
use crate::desktop::keyutils::CMD_OR_CONTROL;
|
||||
|
||||
|
@ -195,25 +194,16 @@ impl Window {
|
|||
webview.notify_keyboard_event(event);
|
||||
}
|
||||
|
||||
fn handle_keyboard_input(
|
||||
&self,
|
||||
servo: &Servo,
|
||||
clipboard: &mut Option<Clipboard>,
|
||||
webviews: &mut WebViewManager,
|
||||
winit_event: KeyEvent,
|
||||
) {
|
||||
fn handle_keyboard_input(&self, state: Rc<RunningAppState>, winit_event: KeyEvent) {
|
||||
// First, handle servoshell key bindings that are not overridable by, or visible to, the page.
|
||||
let mut keyboard_event =
|
||||
keyboard_event_from_winit(&winit_event, self.modifiers_state.get());
|
||||
if self.handle_intercepted_key_bindings(servo, clipboard, webviews, &keyboard_event) {
|
||||
if self.handle_intercepted_key_bindings(state.clone(), &keyboard_event) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Then we deliver character and keyboard events to the page in the focused webview.
|
||||
let Some(webview) = webviews
|
||||
.focused_webview()
|
||||
.map(|webview| webview.servo_webview.clone())
|
||||
else {
|
||||
let Some(webview) = state.focused_webview() else {
|
||||
return;
|
||||
};
|
||||
|
||||
|
@ -296,15 +286,10 @@ impl Window {
|
|||
/// Handle key events before sending them to Servo.
|
||||
fn handle_intercepted_key_bindings(
|
||||
&self,
|
||||
servo: &Servo,
|
||||
clipboard: &mut Option<Clipboard>,
|
||||
webviews: &mut WebViewManager,
|
||||
state: Rc<RunningAppState>,
|
||||
key_event: &KeyboardEvent,
|
||||
) -> bool {
|
||||
let Some(focused_webview) = webviews
|
||||
.focused_webview()
|
||||
.map(|webview| webview.servo_webview.clone())
|
||||
else {
|
||||
let Some(focused_webview) = state.focused_webview() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
|
@ -312,7 +297,7 @@ impl Window {
|
|||
ShortcutMatcher::from_event(key_event.clone())
|
||||
.shortcut(CMD_OR_CONTROL, 'R', || focused_webview.reload())
|
||||
.shortcut(CMD_OR_CONTROL, 'W', || {
|
||||
webviews.close_webview(servo, focused_webview.id());
|
||||
state.close_webview(focused_webview.id());
|
||||
})
|
||||
.shortcut(CMD_OR_CONTROL, 'P', || {
|
||||
let rate = env::var("SAMPLING_RATE")
|
||||
|
@ -335,7 +320,9 @@ impl Window {
|
|||
focused_webview.notify_clipboard_event(ClipboardEventType::Copy);
|
||||
})
|
||||
.shortcut(CMD_OR_CONTROL, 'V', || {
|
||||
let text = clipboard
|
||||
let text = state
|
||||
.inner_mut()
|
||||
.clipboard
|
||||
.as_mut()
|
||||
.and_then(|clipboard| clipboard.get_text().ok())
|
||||
.unwrap_or_default();
|
||||
|
@ -382,40 +369,40 @@ impl Window {
|
|||
|| focused_webview.exit_fullscreen(),
|
||||
)
|
||||
// Select the first 8 tabs via shortcuts
|
||||
.shortcut(CMD_OR_CONTROL, '1', || webviews.focus_webview_by_index(0))
|
||||
.shortcut(CMD_OR_CONTROL, '2', || webviews.focus_webview_by_index(1))
|
||||
.shortcut(CMD_OR_CONTROL, '3', || webviews.focus_webview_by_index(2))
|
||||
.shortcut(CMD_OR_CONTROL, '4', || webviews.focus_webview_by_index(3))
|
||||
.shortcut(CMD_OR_CONTROL, '5', || webviews.focus_webview_by_index(4))
|
||||
.shortcut(CMD_OR_CONTROL, '6', || webviews.focus_webview_by_index(5))
|
||||
.shortcut(CMD_OR_CONTROL, '7', || webviews.focus_webview_by_index(6))
|
||||
.shortcut(CMD_OR_CONTROL, '8', || webviews.focus_webview_by_index(7))
|
||||
.shortcut(CMD_OR_CONTROL, '1', || state.focus_webview_by_index(0))
|
||||
.shortcut(CMD_OR_CONTROL, '2', || state.focus_webview_by_index(1))
|
||||
.shortcut(CMD_OR_CONTROL, '3', || state.focus_webview_by_index(2))
|
||||
.shortcut(CMD_OR_CONTROL, '4', || state.focus_webview_by_index(3))
|
||||
.shortcut(CMD_OR_CONTROL, '5', || state.focus_webview_by_index(4))
|
||||
.shortcut(CMD_OR_CONTROL, '6', || state.focus_webview_by_index(5))
|
||||
.shortcut(CMD_OR_CONTROL, '7', || state.focus_webview_by_index(6))
|
||||
.shortcut(CMD_OR_CONTROL, '8', || state.focus_webview_by_index(7))
|
||||
// Cmd/Ctrl 9 is a bit different in that it focuses the last tab instead of the 9th
|
||||
.shortcut(CMD_OR_CONTROL, '9', || {
|
||||
let len = webviews.webviews().len();
|
||||
let len = state.webviews().len();
|
||||
if len > 0 {
|
||||
webviews.focus_webview_by_index(len - 1)
|
||||
state.focus_webview_by_index(len - 1)
|
||||
}
|
||||
})
|
||||
.shortcut(Modifiers::CONTROL, Key::PageDown, || {
|
||||
if let Some(index) = webviews.get_focused_webview_index() {
|
||||
webviews.focus_webview_by_index((index + 1) % webviews.webviews().len())
|
||||
if let Some(index) = state.get_focused_webview_index() {
|
||||
state.focus_webview_by_index((index + 1) % state.webviews().len())
|
||||
}
|
||||
})
|
||||
.shortcut(Modifiers::CONTROL, Key::PageUp, || {
|
||||
if let Some(index) = webviews.get_focused_webview_index() {
|
||||
if let Some(index) = state.get_focused_webview_index() {
|
||||
let new_index = if index == 0 {
|
||||
webviews.webviews().len() - 1
|
||||
state.webviews().len() - 1
|
||||
} else {
|
||||
index - 1
|
||||
};
|
||||
webviews.focus_webview_by_index(new_index)
|
||||
state.focus_webview_by_index(new_index)
|
||||
}
|
||||
})
|
||||
.shortcut(CMD_OR_CONTROL, 'T', || {
|
||||
webviews.add(servo.new_webview(Url::parse("servo:newtab").unwrap()));
|
||||
state.new_toplevel_webview(Url::parse("servo:newtab").unwrap());
|
||||
})
|
||||
.shortcut(CMD_OR_CONTROL, 'Q', || servo.start_shutting_down())
|
||||
.shortcut(CMD_OR_CONTROL, 'Q', || state.servo().start_shutting_down())
|
||||
.otherwise(|| handled = false);
|
||||
handled
|
||||
}
|
||||
|
@ -442,7 +429,7 @@ impl WindowPortsMethods for Window {
|
|||
self.winit_window.set_title(title);
|
||||
}
|
||||
|
||||
fn request_resize(&self, _: &ServoShellWebView, size: DeviceIntSize) -> Option<DeviceIntSize> {
|
||||
fn request_resize(&self, _: &WebView, size: DeviceIntSize) -> Option<DeviceIntSize> {
|
||||
let toolbar_height = self.toolbar_height() * self.hidpi_factor();
|
||||
let toolbar_height = toolbar_height.get().ceil() as i32;
|
||||
let total_size = PhysicalSize::new(size.width, size.height + toolbar_height);
|
||||
|
@ -536,23 +523,14 @@ impl WindowPortsMethods for Window {
|
|||
self.winit_window.id()
|
||||
}
|
||||
|
||||
fn handle_winit_event(
|
||||
&self,
|
||||
servo: &Servo,
|
||||
clipboard: &mut Option<Clipboard>,
|
||||
webviews: &mut WebViewManager,
|
||||
event: winit::event::WindowEvent,
|
||||
) {
|
||||
let Some(webview) = webviews
|
||||
.focused_webview()
|
||||
.map(|webview| webview.servo_webview.clone())
|
||||
else {
|
||||
fn handle_winit_event(&self, state: Rc<RunningAppState>, event: winit::event::WindowEvent) {
|
||||
let Some(webview) = state.focused_webview() else {
|
||||
return;
|
||||
};
|
||||
|
||||
match event {
|
||||
winit::event::WindowEvent::KeyboardInput { event, .. } => {
|
||||
self.handle_keyboard_input(servo, clipboard, webviews, event)
|
||||
self.handle_keyboard_input(state, event)
|
||||
},
|
||||
winit::event::WindowEvent::ModifiersChanged(modifiers) => {
|
||||
self.modifiers_state.set(modifiers.state())
|
||||
|
@ -615,7 +593,7 @@ impl WindowPortsMethods for Window {
|
|||
webview.set_pinch_zoom(delta as f32 + 1.0);
|
||||
},
|
||||
winit::event::WindowEvent::CloseRequested => {
|
||||
servo.start_shutting_down();
|
||||
state.servo().start_shutting_down();
|
||||
},
|
||||
winit::event::WindowEvent::Resized(new_size) => {
|
||||
if self.inner_size.get() != new_size {
|
||||
|
|
|
@ -7,15 +7,13 @@
|
|||
use std::cell::Cell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use arboard::Clipboard;
|
||||
use euclid::num::Zero;
|
||||
use euclid::{Box2D, Length, Point2D, Scale, Size2D};
|
||||
use servo::compositing::windowing::{AnimationState, EmbedderCoordinates, WindowMethods};
|
||||
use servo::servo_geometry::DeviceIndependentPixel;
|
||||
use servo::webrender_api::units::{DeviceIntSize, DevicePixel};
|
||||
use servo::Servo;
|
||||
|
||||
use super::webview::{WebView, WebViewManager};
|
||||
use super::app_state::RunningAppState;
|
||||
use crate::desktop::window_trait::WindowPortsMethods;
|
||||
|
||||
pub struct Window {
|
||||
|
@ -58,10 +56,6 @@ impl Window {
|
|||
|
||||
Rc::new(window)
|
||||
}
|
||||
|
||||
pub fn new_uninit() -> Rc<dyn WindowPortsMethods> {
|
||||
Self::new(Default::default(), None, None)
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowPortsMethods for Window {
|
||||
|
@ -69,7 +63,11 @@ impl WindowPortsMethods for Window {
|
|||
winit::window::WindowId::dummy()
|
||||
}
|
||||
|
||||
fn request_resize(&self, webview: &WebView, size: DeviceIntSize) -> Option<DeviceIntSize> {
|
||||
fn request_resize(
|
||||
&self,
|
||||
webview: &::servo::WebView,
|
||||
size: DeviceIntSize,
|
||||
) -> Option<DeviceIntSize> {
|
||||
// Surfman doesn't support zero-sized surfaces.
|
||||
let new_size = DeviceIntSize::new(size.width.max(1), size.height.max(1));
|
||||
if self.inner_size.get() == new_size {
|
||||
|
@ -81,7 +79,7 @@ impl WindowPortsMethods for Window {
|
|||
// Because we are managing the rendering surface ourselves, there will be no other
|
||||
// notification (such as from the display manager) that it has changed size, so we
|
||||
// must notify the compositor here.
|
||||
webview.servo_webview.notify_rendering_context_resized();
|
||||
webview.notify_rendering_context_resized();
|
||||
|
||||
Some(new_size)
|
||||
}
|
||||
|
@ -114,13 +112,7 @@ impl WindowPortsMethods for Window {
|
|||
self.animation_state.get() == AnimationState::Animating
|
||||
}
|
||||
|
||||
fn handle_winit_event(
|
||||
&self,
|
||||
_: &Servo,
|
||||
_: &mut Option<Clipboard>,
|
||||
_: &mut WebViewManager,
|
||||
_: winit::event::WindowEvent,
|
||||
) {
|
||||
fn handle_winit_event(&self, _: Rc<RunningAppState>, _: winit::event::WindowEvent) {
|
||||
// Not expecting any winit events.
|
||||
}
|
||||
|
||||
|
|
|
@ -24,14 +24,14 @@ use servo::servo_geometry::DeviceIndependentPixel;
|
|||
use servo::servo_url::ServoUrl;
|
||||
use servo::webrender_api::units::DevicePixel;
|
||||
use servo::webrender_traits::SurfmanRenderingContext;
|
||||
use servo::{LoadStatus, Servo};
|
||||
use servo::{LoadStatus, WebView};
|
||||
use winit::event::{ElementState, MouseButton, WindowEvent};
|
||||
use winit::event_loop::ActiveEventLoop;
|
||||
use winit::window::Window;
|
||||
|
||||
use super::app_state::RunningAppState;
|
||||
use super::egui_glue::EguiGlow;
|
||||
use super::geometry::winit_position_to_euclid_point;
|
||||
use super::webview::{WebView, WebViewManager};
|
||||
|
||||
pub struct Minibrowser {
|
||||
pub context: EguiGlow,
|
||||
|
@ -73,6 +73,12 @@ fn truncate_with_ellipsis(input: &str, max_length: usize) -> String {
|
|||
}
|
||||
}
|
||||
|
||||
impl Drop for Minibrowser {
|
||||
fn drop(&mut self) {
|
||||
self.context.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
impl Minibrowser {
|
||||
pub fn new(
|
||||
rendering_context: &SurfmanRenderingContext,
|
||||
|
@ -173,12 +179,13 @@ impl Minibrowser {
|
|||
/// Draws a browser tab, checking for clicks and queues appropriate `MinibrowserEvent`s.
|
||||
/// Using a custom widget here would've been nice, but it doesn't seem as though egui
|
||||
/// supports that, so we arrange multiple Widgets in a way that they look connected.
|
||||
fn browser_tab(
|
||||
ui: &mut egui::Ui,
|
||||
label: &str,
|
||||
webview: &WebView,
|
||||
event_queue: &mut Vec<MinibrowserEvent>,
|
||||
) {
|
||||
fn browser_tab(ui: &mut egui::Ui, webview: WebView, event_queue: &mut Vec<MinibrowserEvent>) {
|
||||
let label = match (webview.page_title(), webview.url()) {
|
||||
(Some(title), _) if !title.is_empty() => title,
|
||||
(_, Some(url)) => url.to_string(),
|
||||
_ => "New Tab".into(),
|
||||
};
|
||||
|
||||
let old_item_spacing = ui.spacing().item_spacing;
|
||||
let old_visuals = ui.visuals().clone();
|
||||
let active_bg_color = old_visuals.widgets.active.weak_bg_fill;
|
||||
|
@ -214,10 +221,10 @@ impl Minibrowser {
|
|||
visuals.widgets.hovered.rounding = rounding;
|
||||
visuals.widgets.inactive.rounding = rounding;
|
||||
|
||||
let selected = webview.focused;
|
||||
let selected = webview.focused();
|
||||
let tab = ui.add(SelectableLabel::new(
|
||||
selected,
|
||||
truncate_with_ellipsis(label, 20),
|
||||
truncate_with_ellipsis(&label, 20),
|
||||
));
|
||||
let tab = tab.on_hover_ui(|ui| {
|
||||
ui.label(label);
|
||||
|
@ -244,22 +251,16 @@ impl Minibrowser {
|
|||
let close_button = ui.add(egui::Button::new("X").fill(fill_color));
|
||||
*ui.visuals_mut() = old_visuals;
|
||||
if close_button.clicked() || close_button.middle_clicked() || tab.middle_clicked() {
|
||||
event_queue.push(MinibrowserEvent::CloseWebView(webview.servo_webview.id()))
|
||||
event_queue.push(MinibrowserEvent::CloseWebView(webview.id()))
|
||||
} else if !selected && tab.clicked() {
|
||||
webview.servo_webview.focus();
|
||||
webview.focus();
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the minibrowser, but don’t paint.
|
||||
/// If `servo_framebuffer_id` is given, set up a paint callback to blit its contents to our
|
||||
/// CentralPanel when [`Minibrowser::paint`] is called.
|
||||
pub fn update(
|
||||
&mut self,
|
||||
window: &Window,
|
||||
webviews: &mut WebViewManager,
|
||||
servo: Option<&Servo>,
|
||||
reason: &'static str,
|
||||
) {
|
||||
pub fn update(&mut self, window: &Window, state: &RunningAppState, reason: &'static str) {
|
||||
let now = Instant::now();
|
||||
trace!(
|
||||
"{:?} since last update ({})",
|
||||
|
@ -277,9 +278,7 @@ impl Minibrowser {
|
|||
..
|
||||
} = self;
|
||||
let widget_fbo = *widget_surface_fbo;
|
||||
let servo_framebuffer_id = servo
|
||||
.as_ref()
|
||||
.and_then(|servo| servo.offscreen_framebuffer_id());
|
||||
let servo_framebuffer_id = state.servo().offscreen_framebuffer_id();
|
||||
let _duration = context.run(window, |ctx| {
|
||||
// TODO: While in fullscreen add some way to mitigate the increased phishing risk
|
||||
// when not displaying the URL bar: https://github.com/servo/servo/issues/32443
|
||||
|
@ -362,13 +361,8 @@ impl Minibrowser {
|
|||
ui.available_size(),
|
||||
egui::Layout::left_to_right(egui::Align::Center),
|
||||
|ui| {
|
||||
for (_, webview) in webviews.webviews().into_iter() {
|
||||
let label = match (&webview.title, &webview.url) {
|
||||
(Some(title), _) if !title.is_empty() => title,
|
||||
(_, Some(url)) => &url.to_string(),
|
||||
_ => "New Tab",
|
||||
};
|
||||
Self::browser_tab(ui, label, webview, &mut event_queue.borrow_mut());
|
||||
for (_, webview) in state.webviews().into_iter() {
|
||||
Self::browser_tab(ui, webview, &mut event_queue.borrow_mut());
|
||||
}
|
||||
if ui.add(Minibrowser::toolbar_button("+")).clicked() {
|
||||
event_queue.borrow_mut().push(MinibrowserEvent::NewWebView);
|
||||
|
@ -384,17 +378,14 @@ impl Minibrowser {
|
|||
|
||||
let scale =
|
||||
Scale::<_, DeviceIndependentPixel, DevicePixel>::new(ctx.pixels_per_point());
|
||||
let Some(focused_webview_id) = webviews.focused_webview_id() else {
|
||||
return;
|
||||
};
|
||||
let Some(webview) = webviews.get_mut(focused_webview_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
egui::CentralPanel::default().show(ctx, |_| {
|
||||
webview.update(ctx);
|
||||
state.for_each_active_dialog(|dialog| dialog.update(ctx));
|
||||
});
|
||||
|
||||
let Some(webview) = state.focused_webview() else {
|
||||
return;
|
||||
};
|
||||
CentralPanel::default()
|
||||
.frame(Frame::none())
|
||||
.show(ctx, |ui| {
|
||||
|
@ -407,9 +398,8 @@ impl Minibrowser {
|
|||
Point2D::new(x, y),
|
||||
Size2D::new(width, height),
|
||||
) * scale;
|
||||
if rect != webview.rect {
|
||||
webview.rect = rect;
|
||||
webview.servo_webview.move_resize(rect)
|
||||
if rect != webview.rect() {
|
||||
webview.move_resize(rect);
|
||||
}
|
||||
let min = ui.cursor().min;
|
||||
let size = ui.available_size();
|
||||
|
@ -489,13 +479,16 @@ impl Minibrowser {
|
|||
|
||||
/// Updates the location field from the given [WebViewManager], unless the user has started
|
||||
/// editing it without clicking Go, returning true iff it has changed (needing an egui update).
|
||||
pub fn update_location_in_toolbar(&mut self, browser: &mut WebViewManager) -> bool {
|
||||
pub fn update_location_in_toolbar(&mut self, state: &RunningAppState) -> bool {
|
||||
// User edited without clicking Go?
|
||||
if self.location_dirty.get() {
|
||||
return false;
|
||||
}
|
||||
|
||||
match browser.current_url_string() {
|
||||
let current_url_string = state
|
||||
.focused_webview()
|
||||
.and_then(|webview| Some(webview.url()?.to_string()));
|
||||
match current_url_string {
|
||||
Some(location) if location != *self.location.get_mut() => {
|
||||
self.location = RefCell::new(location.to_owned());
|
||||
true
|
||||
|
@ -508,29 +501,32 @@ impl Minibrowser {
|
|||
self.location_dirty.set(dirty);
|
||||
}
|
||||
|
||||
/// Updates the spinner from the given [WebViewManager], returning true iff it has changed
|
||||
/// (needing an egui update).
|
||||
pub fn update_spinner_in_toolbar(&mut self, browser: &mut WebViewManager) -> bool {
|
||||
let need_update = browser.load_status() != self.load_status;
|
||||
self.load_status = browser.load_status();
|
||||
need_update
|
||||
pub fn update_load_status(&mut self, state: &RunningAppState) -> bool {
|
||||
let state_status = state
|
||||
.focused_webview()
|
||||
.map(|webview| webview.load_status())
|
||||
.unwrap_or(LoadStatus::Complete);
|
||||
let old_status = std::mem::replace(&mut self.load_status, state_status);
|
||||
old_status != self.load_status
|
||||
}
|
||||
|
||||
pub fn update_status_text(&mut self, browser: &mut WebViewManager) -> bool {
|
||||
let need_update = browser.status_text() != self.status_text;
|
||||
self.status_text = browser.status_text();
|
||||
need_update
|
||||
pub fn update_status_text(&mut self, state: &RunningAppState) -> bool {
|
||||
let state_status = state
|
||||
.focused_webview()
|
||||
.and_then(|webview| webview.status_text());
|
||||
let old_status = std::mem::replace(&mut self.status_text, state_status);
|
||||
old_status != self.status_text
|
||||
}
|
||||
|
||||
/// Updates all fields taken from the given [WebViewManager], such as the location field.
|
||||
/// Returns true iff the egui needs an update.
|
||||
pub fn update_webview_data(&mut self, browser: &mut WebViewManager) -> bool {
|
||||
pub fn update_webview_data(&mut self, state: &RunningAppState) -> bool {
|
||||
// Note: We must use the "bitwise OR" (|) operator here instead of "logical OR" (||)
|
||||
// because logical OR would short-circuit if any of the functions return true.
|
||||
// We want to ensure that all functions are called. The "bitwise OR" operator
|
||||
// does not short-circuit.
|
||||
self.update_location_in_toolbar(browser) |
|
||||
self.update_spinner_in_toolbar(browser) |
|
||||
self.update_status_text(browser)
|
||||
self.update_location_in_toolbar(state) |
|
||||
self.update_load_status(state) |
|
||||
self.update_status_text(state)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,11 +5,13 @@
|
|||
//! Contains files specific to the servoshell app for Desktop systems.
|
||||
|
||||
pub(crate) mod app;
|
||||
mod app_state;
|
||||
pub(crate) mod cli;
|
||||
mod dialog;
|
||||
mod egui_glue;
|
||||
mod embedder;
|
||||
pub(crate) mod events_loop;
|
||||
mod gamepad;
|
||||
pub mod geometry;
|
||||
mod headed_window;
|
||||
mod headless_window;
|
||||
|
@ -17,5 +19,4 @@ mod keyutils;
|
|||
mod minibrowser;
|
||||
mod protocols;
|
||||
mod tracing;
|
||||
mod webview;
|
||||
mod window_trait;
|
||||
|
|
|
@ -22,21 +22,7 @@ macro_rules! trace_winit_event {
|
|||
};
|
||||
}
|
||||
|
||||
/// Log an event from servo ([servo::EmbedderMsg]) at trace level.
|
||||
/// - To disable tracing: RUST_LOG='servoshell<servo@=off'
|
||||
/// - To enable tracing: RUST_LOG='servoshell<servo@'
|
||||
/// - Recommended filters when tracing is enabled:
|
||||
/// - servoshell<servo@EventDelivered=off
|
||||
/// - servoshell<servo@ReadyToPresent=off
|
||||
macro_rules! trace_embedder_msg {
|
||||
// This macro only exists to put the docs in the same file as the target prefix,
|
||||
// so the macro definition is always the same.
|
||||
($event:expr, $($rest:tt)+) => {
|
||||
::log::trace!(target: $crate::desktop::tracing::LogTarget::log_target(&$event), $($rest)+)
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use {trace_embedder_msg, trace_winit_event};
|
||||
pub(crate) use trace_winit_event;
|
||||
|
||||
/// Get the log target for an event, as a static string.
|
||||
pub(crate) trait LogTarget {
|
||||
|
@ -115,58 +101,3 @@ mod from_winit {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod from_servo {
|
||||
use super::LogTarget;
|
||||
|
||||
macro_rules! target {
|
||||
($($name:literal)+) => {
|
||||
concat!("servoshell<servo@", $($name),+)
|
||||
};
|
||||
}
|
||||
|
||||
impl LogTarget for servo::EmbedderMsg {
|
||||
fn log_target(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Status(..) => target!("Status"),
|
||||
Self::ChangePageTitle(..) => target!("ChangePageTitle"),
|
||||
Self::MoveTo(..) => target!("MoveTo"),
|
||||
Self::ResizeTo(..) => target!("ResizeTo"),
|
||||
Self::Prompt(..) => target!("Prompt"),
|
||||
Self::ShowContextMenu(..) => target!("ShowContextMenu"),
|
||||
Self::AllowNavigationRequest(..) => target!("AllowNavigationRequest"),
|
||||
Self::AllowOpeningWebView(..) => target!("AllowOpeningWebView"),
|
||||
Self::WebViewOpened(..) => target!("WebViewOpened"),
|
||||
Self::WebViewClosed(..) => target!("WebViewClosed"),
|
||||
Self::WebViewFocused(..) => target!("WebViewFocused"),
|
||||
Self::WebViewBlurred => target!("WebViewBlurred"),
|
||||
Self::WebResourceRequested(..) => target!("WebResourceRequested"),
|
||||
Self::AllowUnload(..) => target!("AllowUnload"),
|
||||
Self::Keyboard(..) => target!("Keyboard"),
|
||||
Self::ClearClipboardContents(..) => target!("ClearClipboardContents"),
|
||||
Self::GetClipboardContents(..) => target!("GetClipboardContents"),
|
||||
Self::SetClipboardContents(..) => target!("SetClipboardContents"),
|
||||
Self::SetCursor(..) => target!("SetCursor"),
|
||||
Self::NewFavicon(..) => target!("NewFavicon"),
|
||||
Self::HistoryChanged(..) => target!("HistoryChanged"),
|
||||
Self::SetFullscreenState(..) => target!("SetFullscreenState"),
|
||||
Self::NotifyLoadStatusChanged(..) => target!("NotifyLoadStatusChanged"),
|
||||
Self::Panic(..) => target!("Panic"),
|
||||
Self::GetSelectedBluetoothDevice(..) => target!("GetSelectedBluetoothDevice"),
|
||||
Self::SelectFiles(..) => target!("SelectFiles"),
|
||||
Self::PromptPermission(..) => target!("PromptPermission"),
|
||||
Self::ShowIME(..) => target!("ShowIME"),
|
||||
Self::HideIME(..) => target!("HideIME"),
|
||||
Self::Shutdown => target!("Shutdown"),
|
||||
Self::ReportProfile(..) => target!("ReportProfile"),
|
||||
Self::MediaSessionEvent(..) => target!("MediaSessionEvent"),
|
||||
Self::OnDevtoolsStarted(..) => target!("OnDevtoolsStarted"),
|
||||
Self::RequestDevtoolsConnection(..) => target!("RequestDevtoolsConnection"),
|
||||
Self::ReadyToPresent(..) => target!("ReadyToPresent"),
|
||||
Self::EventDelivered(..) => target!("EventDelivered"),
|
||||
Self::PlayGamepadHapticEffect(..) => target!("PlayGamepadHapticEffect"),
|
||||
Self::StopGamepadHapticEffect(..) => target!("StopGamepadHapticEffect"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,895 +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 std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
use std::{env, thread};
|
||||
|
||||
use arboard::Clipboard;
|
||||
use euclid::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, warn};
|
||||
use servo::base::id::WebViewId;
|
||||
use servo::config::opts::Opts;
|
||||
use servo::ipc_channel::ipc::IpcSender;
|
||||
use servo::servo_url::ServoUrl;
|
||||
use servo::webrender_api::units::DeviceRect;
|
||||
use servo::webrender_api::ScrollLocation;
|
||||
use servo::{
|
||||
CompositorEventVariant, ContextMenuResult, DualRumbleEffectParams, EmbedderMsg, FilterPattern,
|
||||
GamepadEvent, GamepadHapticEffectType, GamepadIndex, GamepadInputBounds,
|
||||
GamepadSupportedHapticEffects, GamepadUpdateType, LoadStatus, PermissionPrompt,
|
||||
PermissionRequest, PromptCredentialsInput, PromptDefinition, PromptOrigin, PromptResult, Servo,
|
||||
TouchEventType,
|
||||
};
|
||||
use tinyfiledialogs::{self, MessageBoxIcon, OkCancel};
|
||||
|
||||
use super::dialog::Dialog;
|
||||
use super::keyutils::CMD_OR_CONTROL;
|
||||
use super::window_trait::{WindowPortsMethods, LINE_HEIGHT};
|
||||
use crate::desktop::tracing::trace_embedder_msg;
|
||||
|
||||
pub struct WebViewManager {
|
||||
status_text: Option<String>,
|
||||
|
||||
/// List of top-level browsing contexts.
|
||||
/// Modified by EmbedderMsg::WebViewOpened and EmbedderMsg::WebViewClosed,
|
||||
/// and we exit if it ever becomes empty.
|
||||
webviews: HashMap<WebViewId, WebView>,
|
||||
|
||||
/// The order in which the webviews were created.
|
||||
creation_order: Vec<WebViewId>,
|
||||
|
||||
/// The webview that is currently focused.
|
||||
/// Modified by EmbedderMsg::WebViewFocused and EmbedderMsg::WebViewBlurred.
|
||||
focused_webview_id: Option<WebViewId>,
|
||||
|
||||
window: Rc<dyn WindowPortsMethods>,
|
||||
gamepad: Option<Gilrs>,
|
||||
haptic_effects: HashMap<usize, HapticEffect>,
|
||||
shutdown_requested: bool,
|
||||
}
|
||||
|
||||
// The state of each Tab/WebView
|
||||
pub struct WebView {
|
||||
pub rect: DeviceRect,
|
||||
pub title: Option<String>,
|
||||
pub url: Option<ServoUrl>,
|
||||
pub focused: bool,
|
||||
pub load_status: LoadStatus,
|
||||
pub servo_webview: ::servo::WebView,
|
||||
dialogs: Vec<Dialog>,
|
||||
}
|
||||
|
||||
impl WebView {
|
||||
fn new(servo_webview: ::servo::WebView) -> Self {
|
||||
Self {
|
||||
rect: DeviceRect::zero(),
|
||||
title: None,
|
||||
url: None,
|
||||
focused: false,
|
||||
load_status: LoadStatus::Complete,
|
||||
servo_webview,
|
||||
dialogs: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_file_dialog(
|
||||
&mut self,
|
||||
multiple: bool,
|
||||
response_sender: IpcSender<Option<Vec<PathBuf>>>,
|
||||
patterns: Vec<FilterPattern>,
|
||||
) {
|
||||
self.dialogs
|
||||
.push(Dialog::new_file_dialog(multiple, response_sender, patterns));
|
||||
}
|
||||
|
||||
pub fn update(&mut self, ctx: &egui::Context) {
|
||||
self.dialogs.retain_mut(|dialog| dialog.update(ctx));
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ServoEventResponse {
|
||||
pub need_present: bool,
|
||||
pub need_update: bool,
|
||||
}
|
||||
|
||||
pub struct HapticEffect {
|
||||
pub effect: Effect,
|
||||
pub sender: IpcSender<bool>,
|
||||
}
|
||||
|
||||
impl WebViewManager {
|
||||
pub fn new() -> WebViewManager {
|
||||
WebViewManager {
|
||||
status_text: None,
|
||||
webviews: HashMap::default(),
|
||||
creation_order: vec![],
|
||||
focused_webview_id: None,
|
||||
window: crate::desktop::headless_window::Window::new_uninit(),
|
||||
gamepad: match Gilrs::new() {
|
||||
Ok(g) => Some(g),
|
||||
Err(e) => {
|
||||
warn!("Error creating gamepad input connection ({})", e);
|
||||
None
|
||||
},
|
||||
},
|
||||
haptic_effects: HashMap::default(),
|
||||
shutdown_requested: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_window(&mut self, window: Rc<dyn WindowPortsMethods>) {
|
||||
self.window = window;
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, webview_id: WebViewId) -> Option<&mut WebView> {
|
||||
self.webviews.get_mut(&webview_id)
|
||||
}
|
||||
|
||||
pub fn get(&self, webview_id: WebViewId) -> Option<&WebView> {
|
||||
self.webviews.get(&webview_id)
|
||||
}
|
||||
|
||||
pub(crate) fn add(&mut self, webview: ::servo::WebView) {
|
||||
self.creation_order.push(webview.id());
|
||||
self.webviews.insert(webview.id(), WebView::new(webview));
|
||||
}
|
||||
|
||||
pub fn focused_webview_id(&self) -> Option<WebViewId> {
|
||||
self.focused_webview_id
|
||||
}
|
||||
|
||||
pub fn close_webview(&mut self, servo: &Servo, webview_id: WebViewId) {
|
||||
// This can happen because we can trigger a close with a UI action and then get the
|
||||
// close event from Servo later.
|
||||
if !self.webviews.contains_key(&webview_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.webviews.retain(|&id, _| id != webview_id);
|
||||
self.creation_order.retain(|&id| id != webview_id);
|
||||
self.focused_webview_id = None;
|
||||
match self.last_created_webview() {
|
||||
Some(last_created_webview) => last_created_webview.servo_webview.focus(),
|
||||
None => servo.start_shutting_down(),
|
||||
}
|
||||
}
|
||||
|
||||
fn last_created_webview(&self) -> Option<&WebView> {
|
||||
self.creation_order
|
||||
.last()
|
||||
.and_then(|id| self.webviews.get(id))
|
||||
}
|
||||
|
||||
pub fn current_url_string(&self) -> Option<String> {
|
||||
match self.focused_webview() {
|
||||
Some(webview) => webview.url.as_ref().map(|url| url.to_string()),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn focused_webview(&self) -> Option<&WebView> {
|
||||
self.focused_webview_id
|
||||
.and_then(|id| self.webviews.get(&id))
|
||||
}
|
||||
|
||||
pub fn load_status(&self) -> LoadStatus {
|
||||
match self.focused_webview() {
|
||||
Some(webview) => webview.load_status,
|
||||
None => LoadStatus::Complete,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn status_text(&self) -> Option<String> {
|
||||
self.status_text.clone()
|
||||
}
|
||||
|
||||
pub fn has_pending_file_dialog(&self) -> bool {
|
||||
self.focused_webview().map_or(false, |webview| {
|
||||
webview
|
||||
.dialogs
|
||||
.iter()
|
||||
.any(|dialog| matches!(dialog, Dialog::File(_)))
|
||||
})
|
||||
}
|
||||
|
||||
// Returns the webviews in the creation order.
|
||||
pub fn webviews(&self) -> Vec<(WebViewId, &WebView)> {
|
||||
let mut res = vec![];
|
||||
for id in &self.creation_order {
|
||||
res.push((*id, self.webviews.get(id).unwrap()))
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
/// Handle updates to connected gamepads from GilRs
|
||||
pub fn handle_gamepad_events(&mut self) {
|
||||
let Some(webview) = self
|
||||
.focused_webview()
|
||||
.map(|webview| webview.servo_webview.clone())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(ref mut gilrs) = self.gamepad {
|
||||
while let Some(event) = gilrs.next_event() {
|
||||
let gamepad = gilrs.gamepad(event.id);
|
||||
let name = gamepad.name();
|
||||
let index = GamepadIndex(event.id.into());
|
||||
let mut gamepad_event: Option<GamepadEvent> = None;
|
||||
match event.event {
|
||||
EventType::ButtonPressed(button, _) => {
|
||||
let mapped_index = Self::map_gamepad_button(button);
|
||||
// We only want to send this for a valid digital button, aka on/off only
|
||||
if !matches!(mapped_index, 6 | 7 | 17) {
|
||||
let update_type = GamepadUpdateType::Button(mapped_index, 1.0);
|
||||
gamepad_event = Some(GamepadEvent::Updated(index, update_type));
|
||||
}
|
||||
},
|
||||
EventType::ButtonReleased(button, _) => {
|
||||
let mapped_index = Self::map_gamepad_button(button);
|
||||
// We only want to send this for a valid digital button, aka on/off only
|
||||
if !matches!(mapped_index, 6 | 7 | 17) {
|
||||
let update_type = GamepadUpdateType::Button(mapped_index, 0.0);
|
||||
gamepad_event = Some(GamepadEvent::Updated(index, update_type));
|
||||
}
|
||||
},
|
||||
EventType::ButtonChanged(button, value, _) => {
|
||||
let mapped_index = Self::map_gamepad_button(button);
|
||||
// We only want to send this for a valid non-digital button, aka the triggers
|
||||
if matches!(mapped_index, 6 | 7) {
|
||||
let update_type = GamepadUpdateType::Button(mapped_index, value as f64);
|
||||
gamepad_event = Some(GamepadEvent::Updated(index, update_type));
|
||||
}
|
||||
},
|
||||
EventType::AxisChanged(axis, value, _) => {
|
||||
// Map axis index and value to represent Standard Gamepad axis
|
||||
// <https://www.w3.org/TR/gamepad/#dfn-represents-a-standard-gamepad-axis>
|
||||
let mapped_axis: usize = match axis {
|
||||
gilrs::Axis::LeftStickX => 0,
|
||||
gilrs::Axis::LeftStickY => 1,
|
||||
gilrs::Axis::RightStickX => 2,
|
||||
gilrs::Axis::RightStickY => 3,
|
||||
_ => 4, // Other axes do not map to "standard" gamepad mapping and are ignored
|
||||
};
|
||||
if mapped_axis < 4 {
|
||||
// The Gamepad spec designates down as positive and up as negative.
|
||||
// GilRs does the inverse of this, so correct for it here.
|
||||
let axis_value = match mapped_axis {
|
||||
0 | 2 => value,
|
||||
1 | 3 => -value,
|
||||
_ => 0., // Should not reach here
|
||||
};
|
||||
let update_type =
|
||||
GamepadUpdateType::Axis(mapped_axis, axis_value as f64);
|
||||
gamepad_event = Some(GamepadEvent::Updated(index, update_type));
|
||||
}
|
||||
},
|
||||
EventType::Connected => {
|
||||
let name = String::from(name);
|
||||
let bounds = GamepadInputBounds {
|
||||
axis_bounds: (-1.0, 1.0),
|
||||
button_bounds: (0.0, 1.0),
|
||||
};
|
||||
// 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());
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
|
||||
if let Some(event) = gamepad_event {
|
||||
webview.notify_gamepad_event(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Map button index and value to represent Standard Gamepad button
|
||||
// <https://www.w3.org/TR/gamepad/#dfn-represents-a-standard-gamepad-button>
|
||||
fn map_gamepad_button(button: gilrs::Button) -> usize {
|
||||
match button {
|
||||
gilrs::Button::South => 0,
|
||||
gilrs::Button::East => 1,
|
||||
gilrs::Button::West => 2,
|
||||
gilrs::Button::North => 3,
|
||||
gilrs::Button::LeftTrigger => 4,
|
||||
gilrs::Button::RightTrigger => 5,
|
||||
gilrs::Button::LeftTrigger2 => 6,
|
||||
gilrs::Button::RightTrigger2 => 7,
|
||||
gilrs::Button::Select => 8,
|
||||
gilrs::Button::Start => 9,
|
||||
gilrs::Button::LeftThumb => 10,
|
||||
gilrs::Button::RightThumb => 11,
|
||||
gilrs::Button::DPadUp => 12,
|
||||
gilrs::Button::DPadDown => 13,
|
||||
gilrs::Button::DPadLeft => 14,
|
||||
gilrs::Button::DPadRight => 15,
|
||||
gilrs::Button::Mode => 16,
|
||||
_ => 17, // Other buttons do not map to "standard" gamepad mapping and are ignored
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
pub(crate) fn focus_webview_by_index(&self, index: usize) {
|
||||
if let Some((_, webview)) = self.webviews().get(index) {
|
||||
webview.servo_webview.focus();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_focused_webview_index(&self) -> Option<usize> {
|
||||
let focused_id = self.focused_webview_id?;
|
||||
self.webviews()
|
||||
.iter()
|
||||
.position(|webview| webview.0 == focused_id)
|
||||
}
|
||||
|
||||
fn send_error(&self, webview_id: WebViewId, error: String) {
|
||||
let Some(webview) = self.get(webview_id) else {
|
||||
return warn!("{error}");
|
||||
};
|
||||
webview.servo_webview.send_error(error);
|
||||
}
|
||||
|
||||
/// Returns true if the caller needs to manually present a new frame.
|
||||
pub fn handle_servo_events(
|
||||
&mut self,
|
||||
servo: &mut Servo,
|
||||
clipboard: &mut Option<Clipboard>,
|
||||
opts: &Opts,
|
||||
messages: Vec<EmbedderMsg>,
|
||||
) -> ServoEventResponse {
|
||||
let mut need_present =
|
||||
self.load_status() != LoadStatus::Complete || self.has_pending_file_dialog();
|
||||
let mut need_update = false;
|
||||
for message in messages {
|
||||
trace_embedder_msg!(message, "{message:?}");
|
||||
|
||||
match message {
|
||||
EmbedderMsg::Status(_, status) => {
|
||||
self.status_text = status;
|
||||
need_update = true;
|
||||
},
|
||||
EmbedderMsg::ChangePageTitle(webview_id, title) => {
|
||||
// Set the title to the target webview, and update the OS window title
|
||||
// if this is the currently focused one.
|
||||
if let Some(webview) = self.get_mut(webview_id) {
|
||||
webview.title = title.clone();
|
||||
if webview.focused {
|
||||
self.window.set_title(&format!(
|
||||
"{} - Servo",
|
||||
title.clone().unwrap_or_default()
|
||||
));
|
||||
}
|
||||
need_update = true;
|
||||
}
|
||||
},
|
||||
EmbedderMsg::MoveTo(_, point) => {
|
||||
self.window.set_position(point);
|
||||
},
|
||||
EmbedderMsg::ResizeTo(webview_id, inner_size) => {
|
||||
if let Some(webview) = self.get_mut(webview_id) {
|
||||
if webview.rect.size() != inner_size.to_f32() {
|
||||
webview.rect.set_size(inner_size.to_f32());
|
||||
webview.servo_webview.move_resize(webview.rect);
|
||||
}
|
||||
};
|
||||
if let Some(webview) = self.get(webview_id) {
|
||||
self.window.request_resize(webview, inner_size);
|
||||
}
|
||||
},
|
||||
EmbedderMsg::Prompt(webview_id, definition, origin) => {
|
||||
let res = if opts.headless {
|
||||
match definition {
|
||||
PromptDefinition::Alert(_message, sender) => sender.send(()),
|
||||
PromptDefinition::OkCancel(_message, sender) => {
|
||||
sender.send(PromptResult::Primary)
|
||||
},
|
||||
PromptDefinition::Input(_message, default, sender) => {
|
||||
sender.send(Some(default.to_owned()))
|
||||
},
|
||||
PromptDefinition::Credentials(sender) => {
|
||||
sender.send(PromptCredentialsInput {
|
||||
username: None,
|
||||
password: None,
|
||||
})
|
||||
},
|
||||
}
|
||||
} else {
|
||||
thread::Builder::new()
|
||||
.name("AlertDialog".to_owned())
|
||||
.spawn(move || match definition {
|
||||
PromptDefinition::Alert(mut message, sender) => {
|
||||
if origin == PromptOrigin::Untrusted {
|
||||
message = tiny_dialog_escape(&message);
|
||||
}
|
||||
tinyfiledialogs::message_box_ok(
|
||||
"Alert!",
|
||||
&message,
|
||||
MessageBoxIcon::Warning,
|
||||
);
|
||||
sender.send(())
|
||||
},
|
||||
PromptDefinition::OkCancel(mut message, sender) => {
|
||||
if origin == PromptOrigin::Untrusted {
|
||||
message = tiny_dialog_escape(&message);
|
||||
}
|
||||
let result = tinyfiledialogs::message_box_ok_cancel(
|
||||
"",
|
||||
&message,
|
||||
MessageBoxIcon::Warning,
|
||||
OkCancel::Cancel,
|
||||
);
|
||||
sender.send(match result {
|
||||
OkCancel::Ok => PromptResult::Primary,
|
||||
OkCancel::Cancel => PromptResult::Secondary,
|
||||
})
|
||||
},
|
||||
PromptDefinition::Input(mut message, mut default, sender) => {
|
||||
if origin == PromptOrigin::Untrusted {
|
||||
message = tiny_dialog_escape(&message);
|
||||
default = tiny_dialog_escape(&default);
|
||||
}
|
||||
let result = tinyfiledialogs::input_box("", &message, &default);
|
||||
sender.send(result)
|
||||
},
|
||||
PromptDefinition::Credentials(sender) => {
|
||||
// TODO: figure out how to make the message a localized string
|
||||
let username = tinyfiledialogs::input_box("", "username", "");
|
||||
let password = tinyfiledialogs::input_box("", "password", "");
|
||||
sender.send(PromptCredentialsInput { username, password })
|
||||
},
|
||||
})
|
||||
.unwrap()
|
||||
.join()
|
||||
.expect("Thread spawning failed")
|
||||
};
|
||||
if let Err(e) = res {
|
||||
self.send_error(webview_id, format!("Failed to send Prompt response: {e}"))
|
||||
}
|
||||
},
|
||||
EmbedderMsg::AllowUnload(webview_id, sender) => {
|
||||
// Always allow unload for now.
|
||||
if let Err(e) = sender.send(true) {
|
||||
self.send_error(
|
||||
webview_id,
|
||||
format!("Failed to send AllowUnload response: {e}"),
|
||||
)
|
||||
}
|
||||
},
|
||||
EmbedderMsg::AllowNavigationRequest(_, pipeline_id, _url) => {
|
||||
servo.allow_navigation_response(pipeline_id, true);
|
||||
},
|
||||
EmbedderMsg::AllowOpeningWebView(_, response_chan) => {
|
||||
let webview = servo.new_auxiliary_webview();
|
||||
match response_chan.send(Some(webview.id())) {
|
||||
Ok(()) => self.add(webview),
|
||||
Err(error) => warn!("Failed to send AllowOpeningWebView response: {error}"),
|
||||
}
|
||||
},
|
||||
EmbedderMsg::WebViewOpened(new_webview_id) => {
|
||||
let scale = self.window.hidpi_factor().get();
|
||||
let toolbar = self.window.toolbar_height().get();
|
||||
|
||||
// Adjust for our toolbar height.
|
||||
// TODO: Adjust for egui window decorations if we end up using those
|
||||
let mut rect = self.window.get_coordinates().get_viewport().to_f32();
|
||||
rect.min.y += toolbar * scale;
|
||||
|
||||
let webview = self
|
||||
.webviews
|
||||
.get(&new_webview_id)
|
||||
.expect("Unknown webview opened.");
|
||||
webview.servo_webview.focus();
|
||||
webview.servo_webview.move_resize(rect);
|
||||
webview.servo_webview.raise_to_top(true);
|
||||
},
|
||||
EmbedderMsg::WebViewClosed(webview_id) => {
|
||||
self.close_webview(servo, webview_id);
|
||||
},
|
||||
EmbedderMsg::WebViewFocused(webview_id) => {
|
||||
if let Some(webview) = self.focused_webview_id.and_then(|id| self.get_mut(id)) {
|
||||
webview.focused = false;
|
||||
}
|
||||
|
||||
// Show the most recently created webview and hide all others.
|
||||
// TODO: Stop doing this once we have full multiple webviews support
|
||||
if let Some(webview) = self.get_mut(webview_id) {
|
||||
webview.focused = true;
|
||||
webview.servo_webview.show(true);
|
||||
self.focused_webview_id = Some(webview_id);
|
||||
need_update = true;
|
||||
};
|
||||
},
|
||||
EmbedderMsg::WebViewBlurred => {
|
||||
for webview in self.webviews.values_mut() {
|
||||
webview.focused = false;
|
||||
}
|
||||
self.focused_webview_id = None;
|
||||
},
|
||||
EmbedderMsg::Keyboard(webview_id, key_event) => {
|
||||
self.handle_overridable_key_bindings(webview_id, key_event);
|
||||
},
|
||||
EmbedderMsg::ClearClipboardContents(_) => {
|
||||
clipboard
|
||||
.as_mut()
|
||||
.and_then(|clipboard| clipboard.clear().ok());
|
||||
},
|
||||
EmbedderMsg::GetClipboardContents(_, sender) => {
|
||||
let contents = clipboard
|
||||
.as_mut()
|
||||
.and_then(|clipboard| clipboard.get_text().ok())
|
||||
.unwrap_or_default();
|
||||
if let Err(e) = sender.send(contents) {
|
||||
warn!("Failed to send clipboard ({})", e);
|
||||
}
|
||||
},
|
||||
EmbedderMsg::SetClipboardContents(_, text) => {
|
||||
if let Some(clipboard) = clipboard.as_mut() {
|
||||
if let Err(e) = clipboard.set_text(text) {
|
||||
warn!("Error setting clipboard contents ({})", e);
|
||||
}
|
||||
}
|
||||
},
|
||||
EmbedderMsg::SetCursor(_, cursor) => {
|
||||
self.window.set_cursor(cursor);
|
||||
},
|
||||
EmbedderMsg::NewFavicon(_, _url) => {
|
||||
// FIXME: show favicons in the UI somehow
|
||||
},
|
||||
EmbedderMsg::NotifyLoadStatusChanged(webview_id, load_status) => {
|
||||
if let Some(webview) = self.get_mut(webview_id) {
|
||||
webview.load_status = load_status;
|
||||
need_update = true;
|
||||
};
|
||||
},
|
||||
EmbedderMsg::HistoryChanged(webview_id, urls, current) => {
|
||||
if let Some(webview) = self.get_mut(webview_id) {
|
||||
webview.url = Some(urls[current].clone());
|
||||
need_update = true;
|
||||
};
|
||||
},
|
||||
EmbedderMsg::SetFullscreenState(_, state) => {
|
||||
self.window.set_fullscreen(state);
|
||||
},
|
||||
EmbedderMsg::WebResourceRequested(_, _web_resource_request, _response_sender) => {},
|
||||
EmbedderMsg::Shutdown => {
|
||||
self.shutdown_requested = true;
|
||||
},
|
||||
EmbedderMsg::Panic(_, _reason, _backtrace) => {},
|
||||
EmbedderMsg::GetSelectedBluetoothDevice(webview_id, devices, sender) => {
|
||||
let selected = platform_get_selected_devices(devices);
|
||||
if let Err(e) = sender.send(selected) {
|
||||
self.send_error(
|
||||
webview_id,
|
||||
format!("Failed to send GetSelectedBluetoothDevice response: {e}"),
|
||||
);
|
||||
};
|
||||
},
|
||||
EmbedderMsg::SelectFiles(webview_id, patterns, multiple_files, sender) => {
|
||||
if let Some(webview) = self.get_mut(webview_id) {
|
||||
webview.add_file_dialog(multiple_files, sender, patterns);
|
||||
need_update = true;
|
||||
need_present = true;
|
||||
};
|
||||
},
|
||||
EmbedderMsg::PromptPermission(_, prompt, sender) => {
|
||||
let _ = sender.send(match opts.headless {
|
||||
true => PermissionRequest::Denied,
|
||||
false => prompt_user(prompt),
|
||||
});
|
||||
},
|
||||
EmbedderMsg::ShowIME(_webview_id, _kind, _text, _multiline, _rect) => {
|
||||
debug!("ShowIME received");
|
||||
},
|
||||
EmbedderMsg::HideIME(_webview_id) => {
|
||||
debug!("HideIME received");
|
||||
},
|
||||
EmbedderMsg::ReportProfile(bytes) => {
|
||||
let filename = env::var("PROFILE_OUTPUT").unwrap_or("samples.json".to_string());
|
||||
let result = File::create(&filename).and_then(|mut f| f.write_all(&bytes));
|
||||
if let Err(e) = result {
|
||||
error!("Failed to store profile: {}", e);
|
||||
}
|
||||
},
|
||||
EmbedderMsg::MediaSessionEvent(..) => {
|
||||
debug!("MediaSessionEvent received");
|
||||
// TODO(ferjm): MediaSession support for winit based browsers.
|
||||
},
|
||||
EmbedderMsg::OnDevtoolsStarted(port, _token) => match port {
|
||||
Ok(p) => info!("Devtools Server running on port {}", p),
|
||||
Err(()) => error!("Error running devtools server"),
|
||||
},
|
||||
EmbedderMsg::RequestDevtoolsConnection(response_sender) => {
|
||||
let _ = response_sender.send(true);
|
||||
},
|
||||
EmbedderMsg::ShowContextMenu(_, sender, ..) => {
|
||||
let _ = sender.send(ContextMenuResult::Ignored);
|
||||
},
|
||||
EmbedderMsg::ReadyToPresent(_webview_ids) => {
|
||||
need_present = true;
|
||||
},
|
||||
EmbedderMsg::EventDelivered(webview_id, event) => {
|
||||
if let Some(webview) = self.get_mut(webview_id) {
|
||||
if let CompositorEventVariant::MouseButtonEvent = event {
|
||||
webview.servo_webview.raise_to_top(true);
|
||||
webview.servo_webview.focus();
|
||||
}
|
||||
};
|
||||
},
|
||||
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");
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
ServoEventResponse {
|
||||
need_present,
|
||||
need_update,
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle servoshell key bindings that may have been prevented by the page in the focused webview.
|
||||
fn handle_overridable_key_bindings(&mut self, webview_id: WebViewId, event: KeyboardEvent) {
|
||||
let Some(webview) = self.get(webview_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let origin = webview.rect.min.ceil().to_i32();
|
||||
let webview = &webview.servo_webview;
|
||||
ShortcutMatcher::from_event(event)
|
||||
.shortcut(CMD_OR_CONTROL, '=', || {
|
||||
webview.set_zoom(1.1);
|
||||
})
|
||||
.shortcut(CMD_OR_CONTROL, '+', || {
|
||||
webview.set_zoom(1.1);
|
||||
})
|
||||
.shortcut(CMD_OR_CONTROL, '-', || {
|
||||
webview.set_zoom(1.0 / 1.1);
|
||||
})
|
||||
.shortcut(CMD_OR_CONTROL, '0', || {
|
||||
webview.reset_zoom();
|
||||
})
|
||||
.shortcut(Modifiers::empty(), Key::PageDown, || {
|
||||
let scroll_location = ScrollLocation::Delta(Vector2D::new(
|
||||
0.0,
|
||||
-self.window.page_height() + 2.0 * LINE_HEIGHT,
|
||||
));
|
||||
webview.notify_scroll_event(scroll_location, origin, TouchEventType::Move);
|
||||
})
|
||||
.shortcut(Modifiers::empty(), Key::PageUp, || {
|
||||
let scroll_location = ScrollLocation::Delta(Vector2D::new(
|
||||
0.0,
|
||||
self.window.page_height() - 2.0 * LINE_HEIGHT,
|
||||
));
|
||||
webview.notify_scroll_event(scroll_location, origin, TouchEventType::Move);
|
||||
})
|
||||
.shortcut(Modifiers::empty(), Key::Home, || {
|
||||
webview.notify_scroll_event(ScrollLocation::Start, origin, TouchEventType::Move);
|
||||
})
|
||||
.shortcut(Modifiers::empty(), Key::End, || {
|
||||
webview.notify_scroll_event(ScrollLocation::End, origin, TouchEventType::Move);
|
||||
})
|
||||
.shortcut(Modifiers::empty(), Key::ArrowUp, || {
|
||||
let location = ScrollLocation::Delta(Vector2D::new(0.0, 3.0 * LINE_HEIGHT));
|
||||
webview.notify_scroll_event(location, origin, TouchEventType::Move);
|
||||
})
|
||||
.shortcut(Modifiers::empty(), Key::ArrowDown, || {
|
||||
let location = ScrollLocation::Delta(Vector2D::new(0.0, -3.0 * LINE_HEIGHT));
|
||||
webview.notify_scroll_event(location, origin, TouchEventType::Move);
|
||||
})
|
||||
.shortcut(Modifiers::empty(), Key::ArrowLeft, || {
|
||||
let location = ScrollLocation::Delta(Vector2D::new(LINE_HEIGHT, 0.0));
|
||||
webview.notify_scroll_event(location, origin, TouchEventType::Move);
|
||||
})
|
||||
.shortcut(Modifiers::empty(), Key::ArrowRight, || {
|
||||
let location = ScrollLocation::Delta(Vector2D::new(-LINE_HEIGHT, 0.0));
|
||||
webview.notify_scroll_event(location, origin, TouchEventType::Move);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn prompt_user(prompt: PermissionPrompt) -> PermissionRequest {
|
||||
use tinyfiledialogs::YesNo;
|
||||
|
||||
let message = match prompt {
|
||||
PermissionPrompt::Request(permission_name) => {
|
||||
format!("Do you want to grant permission for {:?}?", permission_name)
|
||||
},
|
||||
PermissionPrompt::Insecure(permission_name) => {
|
||||
format!(
|
||||
"The {:?} feature is only safe to use in secure context, but servo can't guarantee\n\
|
||||
that the current context is secure. Do you want to proceed and grant permission?",
|
||||
permission_name
|
||||
)
|
||||
},
|
||||
};
|
||||
|
||||
match tinyfiledialogs::message_box_yes_no(
|
||||
"Permission request dialog",
|
||||
&message,
|
||||
MessageBoxIcon::Question,
|
||||
YesNo::No,
|
||||
) {
|
||||
YesNo::Yes => PermissionRequest::Granted,
|
||||
YesNo::No => PermissionRequest::Denied,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn prompt_user(_prompt: PermissionPrompt) -> PermissionRequest {
|
||||
// TODO popup only supported on linux
|
||||
PermissionRequest::Denied
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn platform_get_selected_devices(devices: Vec<String>) -> Option<String> {
|
||||
thread::Builder::new()
|
||||
.name("DevicePicker".to_owned())
|
||||
.spawn(move || {
|
||||
let dialog_rows: Vec<&str> = devices.iter().map(|s| s.as_ref()).collect();
|
||||
let dialog_rows: Option<&[&str]> = Some(dialog_rows.as_slice());
|
||||
|
||||
match tinyfiledialogs::list_dialog("Choose a device", &["Id", "Name"], dialog_rows) {
|
||||
Some(device) => {
|
||||
// The device string format will be "Address|Name". We need the first part of it.
|
||||
device.split('|').next().map(|s| s.to_string())
|
||||
},
|
||||
None => None,
|
||||
}
|
||||
})
|
||||
.unwrap()
|
||||
.join()
|
||||
.expect("Thread spawning failed")
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn platform_get_selected_devices(devices: Vec<String>) -> Option<String> {
|
||||
for device in devices {
|
||||
if let Some(address) = device.split('|').next().map(|s| s.to_string()) {
|
||||
return Some(address);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
// This is a mitigation for #25498, not a verified solution.
|
||||
// There may be codepaths in tinyfiledialog.c that this is
|
||||
// inadquate against, as it passes the string via shell to
|
||||
// different programs depending on what the user has installed.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn tiny_dialog_escape(raw: &str) -> String {
|
||||
let s: String = raw
|
||||
.chars()
|
||||
.filter_map(|c| match c {
|
||||
'\n' => Some('\n'),
|
||||
'\0'..='\x1f' => None,
|
||||
'<' => Some('\u{FF1C}'),
|
||||
'>' => Some('\u{FF1E}'),
|
||||
'&' => Some('\u{FF06}'),
|
||||
_ => Some(c),
|
||||
})
|
||||
.collect();
|
||||
shellwords::escape(&s)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn tiny_dialog_escape(raw: &str) -> String {
|
||||
raw.to_string()
|
||||
}
|
|
@ -7,14 +7,13 @@
|
|||
|
||||
use std::rc::Rc;
|
||||
|
||||
use arboard::Clipboard;
|
||||
use euclid::{Length, Scale};
|
||||
use servo::compositing::windowing::WindowMethods;
|
||||
use servo::servo_geometry::DeviceIndependentPixel;
|
||||
use servo::webrender_api::units::{DeviceIntPoint, DeviceIntSize, DevicePixel};
|
||||
use servo::{Cursor, Servo};
|
||||
use servo::{Cursor, WebView};
|
||||
|
||||
use super::webview::{WebView, WebViewManager};
|
||||
use super::app_state::RunningAppState;
|
||||
|
||||
// This should vary by zoom level and maybe actual text size (focused or under cursor)
|
||||
pub const LINE_HEIGHT: f32 = 38.0;
|
||||
|
@ -31,13 +30,7 @@ pub trait WindowPortsMethods: WindowMethods {
|
|||
) -> Option<Scale<f32, DeviceIndependentPixel, DevicePixel>>;
|
||||
fn page_height(&self) -> f32;
|
||||
fn get_fullscreen(&self) -> bool;
|
||||
fn handle_winit_event(
|
||||
&self,
|
||||
servo: &Servo,
|
||||
clipboard: &mut Option<Clipboard>,
|
||||
webviews: &mut WebViewManager,
|
||||
event: winit::event::WindowEvent,
|
||||
);
|
||||
fn handle_winit_event(&self, state: Rc<RunningAppState>, event: winit::event::WindowEvent);
|
||||
fn is_animating(&self) -> bool;
|
||||
fn set_title(&self, _title: &str) {}
|
||||
/// Request a new inner size for the window, not including external decorations.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue