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:
Martin Robinson 2025-02-06 08:33:31 +01:00 committed by GitHub
parent 6b12499077
commit 5f08e4fa76
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 1109 additions and 1258 deletions

View file

@ -1089,7 +1089,11 @@ impl Servo {
webview.delegate().notify_ready_to_show(webview); 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) => { EmbedderMsg::WebViewFocused(webview_id) => {
for id in self.webviews.borrow().keys() { for id in self.webviews.borrow().keys() {
if let Some(webview) = self.get_webview_handle(*id) { if let Some(webview) = self.get_webview_handle(*id) {

View file

@ -117,6 +117,10 @@ pub trait WebViewDelegate {
/// The history state has changed. /// The history state has changed.
// changed pattern; maybe wasteful if embedder doesnt care? // changed pattern; maybe wasteful if embedder doesnt care?
fn notify_history_changed(&self, _webview: WebView, _: Vec<Url>, _: usize) {} 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 /// 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`] /// 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> { fn request_open_auxiliary_webview(&self, _parent_webview: WebView) -> Option<WebView> {
None 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. /// Open interface to request permission specified by prompt.
fn request_permission( fn request_permission(
&self, &self,

View file

@ -10,7 +10,6 @@ use std::rc::Rc;
use std::time::Instant; use std::time::Instant;
use std::{env, fs}; use std::{env, fs};
use arboard::Clipboard;
use log::{info, trace, warn}; use log::{info, trace, warn};
use raw_window_handle::HasDisplayHandle; use raw_window_handle::HasDisplayHandle;
use servo::compositing::windowing::{AnimationState, WindowMethods}; use servo::compositing::windowing::{AnimationState, WindowMethods};
@ -31,10 +30,11 @@ use winit::event::WindowEvent;
use winit::event_loop::{ActiveEventLoop, ControlFlow}; use winit::event_loop::{ActiveEventLoop, ControlFlow};
use winit::window::WindowId; use winit::window::WindowId;
use super::app_state::AppState;
use super::events_loop::{EventsLoop, WakerEvent}; use super::events_loop::{EventsLoop, WakerEvent};
use super::minibrowser::{Minibrowser, MinibrowserEvent}; use super::minibrowser::{Minibrowser, MinibrowserEvent};
use super::webview::WebViewManager;
use super::{headed_window, headless_window}; use super::{headed_window, headless_window};
use crate::desktop::app_state::RunningAppState;
use crate::desktop::embedder::{EmbedderCallbacks, XrDiscovery}; use crate::desktop::embedder::{EmbedderCallbacks, XrDiscovery};
use crate::desktop::tracing::trace_winit_event; use crate::desktop::tracing::trace_winit_event;
use crate::desktop::window_trait::WindowPortsMethods; use crate::desktop::window_trait::WindowPortsMethods;
@ -45,9 +45,6 @@ pub struct App {
opts: Opts, opts: Opts,
preferences: Preferences, preferences: Preferences,
servo_shell_preferences: ServoShellPreferences, servo_shell_preferences: ServoShellPreferences,
clipboard: Option<Clipboard>,
servo: Option<Servo>,
webviews: WebViewManager,
suspended: Cell<bool>, suspended: Cell<bool>,
windows: HashMap<WindowId, Rc<dyn WindowPortsMethods>>, windows: HashMap<WindowId, Rc<dyn WindowPortsMethods>>,
minibrowser: Option<Minibrowser>, minibrowser: Option<Minibrowser>,
@ -55,15 +52,16 @@ pub struct App {
initial_url: ServoUrl, initial_url: ServoUrl,
t_start: Instant, t_start: Instant,
t: Instant, t: Instant,
state: AppState,
} }
enum Present { pub(crate) enum Present {
Deferred, Deferred,
None, None,
} }
/// Action to be taken by the caller of [`App::handle_events`]. /// 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. /// The caller should shut down Servo and its related context.
Shutdown, Shutdown,
Continue { Continue {
@ -91,9 +89,6 @@ impl App {
opts, opts,
preferences, preferences,
servo_shell_preferences, servo_shell_preferences,
clipboard: Clipboard::new().ok(),
webviews: WebViewManager::new(),
servo: None,
suspended: Cell::new(false), suspended: Cell::new(false),
windows: HashMap::new(), windows: HashMap::new(),
minibrowser: None, minibrowser: None,
@ -101,6 +96,7 @@ impl App {
initial_url: initial_url.clone(), initial_url: initial_url.clone(),
t_start: t, t_start: t,
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() { if window.winit_window().is_some() {
self.minibrowser = Some(Minibrowser::new( self.minibrowser = Some(Minibrowser::new(
&rendering_context, &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.windows.insert(window.id(), window);
self.suspended.set(false); self.suspended.set(false);
@ -192,6 +175,15 @@ impl App {
None 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 // TODO: Remove this once dyn upcasting coercion stabilises
// <https://github.com/rust-lang/rust/issues/65991> // <https://github.com/rust-lang/rust/issues/65991>
struct UpcastedWindow(Rc<dyn WindowPortsMethods>); struct UpcastedWindow(Rc<dyn WindowPortsMethods>);
@ -203,116 +195,61 @@ impl App {
self.0.set_animation_state(state); 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( let servo = Servo::new(
self.opts.clone(), self.opts.clone(),
self.preferences.clone(), self.preferences.clone(),
Rc::new(rendering_context), Rc::new(rendering_context),
embedder, embedder,
Rc::new(window), Rc::new(UpcastedWindow(window.clone())),
self.servo_shell_preferences.user_agent.clone(), self.servo_shell_preferences.user_agent.clone(),
composite_target, composite_target,
); );
servo.setup_logging(); servo.setup_logging();
let webview = servo.new_webview(self.initial_url.clone().into_url()); let running_state = Rc::new(RunningAppState::new(
self.webviews.add(webview); 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 { pub fn is_animating(&self) -> bool {
self.windows.iter().any(|(_, window)| window.is_animating()) 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 /// Handle events with winit contexts
pub fn handle_events_with_winit( pub fn handle_events_with_winit(
&mut self, &mut self,
event_loop: &ActiveEventLoop, event_loop: &ActiveEventLoop,
window: Rc<dyn WindowPortsMethods>, window: Rc<dyn WindowPortsMethods>,
) { ) {
match self.handle_events() { let AppState::Running(state) = &self.state else {
return;
};
match state.pump_event_loop() {
PumpResult::Shutdown => { PumpResult::Shutdown => {
event_loop.exit(); state.shutdown();
self.servo.take().unwrap().deinit(); self.state = AppState::ShuttingDown;
if let Some(ref mut minibrowser) = self.minibrowser {
minibrowser.context.destroy();
}
}, },
PumpResult::Continue { update, present } => { PumpResult::Continue { update, present } => {
if update { if update {
if let Some(ref mut minibrowser) = self.minibrowser { 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 // Update the minibrowser immediately. While we could update by requesting a
// redraw, doing so would delay the location update by two frames. // redraw, doing so would delay the location update by two frames.
minibrowser.update( minibrowser.update(
window.winit_window().unwrap(), window.winit_window().unwrap(),
&mut self.webviews, state,
self.servo.as_ref(),
"update_location_in_toolbar", "update_location_in_toolbar",
); );
} }
@ -327,16 +264,21 @@ impl App {
if let Some(window) = window.winit_window() { if let Some(window) = window.winit_window() {
window.request_redraw(); window.request_redraw();
} else { } else {
self.servo.as_mut().unwrap().present(); state.servo().present();
} }
}, },
Present::None => {}, 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 { pub fn handle_events_with_headless(&mut self) -> bool {
let now = Instant::now(); let now = Instant::now();
let event = winit::event::Event::UserEvent(WakerEvent); let event = winit::event::Event::UserEvent(WakerEvent);
@ -347,20 +289,16 @@ impl App {
now - self.t now - self.t
); );
self.t = now; 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; // We should always be in the running state.
match self.handle_events() { let AppState::Running(state) = &self.state else {
return false;
};
match state.pump_event_loop() {
PumpResult::Shutdown => { PumpResult::Shutdown => {
exit = true; state.shutdown();
self.servo.take().unwrap().deinit(); self.state = AppState::ShuttingDown;
if let Some(ref mut minibrowser) = self.minibrowser {
minibrowser.context.destroy();
}
}, },
PumpResult::Continue { present, .. } => { PumpResult::Continue { present, .. } => {
match present { match present {
@ -368,13 +306,14 @@ impl App {
// The compositor has painted to this frame. // The compositor has painted to this frame.
trace!("PumpResult::Present::Deferred"); trace!("PumpResult::Present::Deferred");
// In headless mode, we present directly. // In headless mode, we present directly.
self.servo.as_mut().unwrap().present(); state.servo().present();
}, },
Present::None => {}, Present::None => {},
} }
}, },
} }
exit
!matches!(self.state, AppState::ShuttingDown)
} }
/// Takes any events generated during `egui` updates and performs their actions. /// 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 { let Some(minibrowser) = self.minibrowser.as_ref() else {
return; 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; return;
}; };
@ -397,34 +337,33 @@ impl App {
warn!("failed to parse location"); warn!("failed to parse location");
break; break;
}; };
if let Some(focused_webview) = self.webviews.focused_webview() { if let Some(focused_webview) = state.focused_webview() {
focused_webview.servo_webview.load(url.into_url()); focused_webview.load(url.into_url());
} }
}, },
MinibrowserEvent::Back => { MinibrowserEvent::Back => {
if let Some(focused_webview) = self.webviews.focused_webview() { if let Some(focused_webview) = state.focused_webview() {
focused_webview.servo_webview.go_back(1); focused_webview.go_back(1);
} }
}, },
MinibrowserEvent::Forward => { MinibrowserEvent::Forward => {
if let Some(focused_webview) = self.webviews.focused_webview() { if let Some(focused_webview) = state.focused_webview() {
focused_webview.servo_webview.go_forward(1); focused_webview.go_forward(1);
} }
}, },
MinibrowserEvent::Reload => { MinibrowserEvent::Reload => {
minibrowser.update_location_dirty(false); minibrowser.update_location_dirty(false);
if let Some(focused_webview) = self.webviews.focused_webview() { if let Some(focused_webview) = state.focused_webview() {
focused_webview.servo_webview.reload(); focused_webview.reload();
} }
}, },
MinibrowserEvent::NewWebView => { MinibrowserEvent::NewWebView => {
minibrowser.update_location_dirty(false); minibrowser.update_location_dirty(false);
let webview = servo.new_webview(Url::parse("servo:newtab").unwrap()); state.new_toplevel_webview(Url::parse("servo:newtab").unwrap());
self.webviews.add(webview);
}, },
MinibrowserEvent::CloseWebView(id) => { MinibrowserEvent::CloseWebView(id) => {
minibrowser.update_location_dirty(false); 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 now - self.t
); );
self.t = now; 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 AppState::Running(state) = &self.state else {
let Some(ref mut servo) = self.servo else {
return; return;
}; };
let Some(window) = self.windows.get(&window_id) else { let Some(window) = self.windows.get(&window_id) else {
return; return;
}; };
let window = window.clone();
let window = window.clone();
if event == winit::event::WindowEvent::RedrawRequested { if event == winit::event::WindowEvent::RedrawRequested {
// We need to redraw the window for some reason. // We need to redraw the window for some reason.
trace!("RedrawRequested"); 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 // WARNING: do not defer painting or presenting to some later tick of the event
// loop or servoshell may become unresponsive! (servo#30312) // loop or servoshell may become unresponsive! (servo#30312)
if let Some(ref mut minibrowser) = self.minibrowser { if let Some(ref mut minibrowser) = self.minibrowser {
minibrowser.update( minibrowser.update(window.winit_window().unwrap(), state, "RedrawRequested");
window.winit_window().unwrap(),
&mut self.webviews,
Some(servo),
"RedrawRequested",
);
minibrowser.paint(window.winit_window().unwrap()); minibrowser.paint(window.winit_window().unwrap());
} }
servo.present(); state.servo().present();
} }
// Handle the event // Handle the event
@ -512,8 +445,7 @@ impl ApplicationHandler<WakerEvent> for App {
if let WindowEvent::Resized(_) = event { if let WindowEvent::Resized(_) = event {
minibrowser.update( minibrowser.update(
window.winit_window().unwrap(), window.winit_window().unwrap(),
&mut self.webviews, state,
Some(servo),
"Sync WebView size with Window Resize event", "Sync WebView size with Window Resize event",
); );
} }
@ -530,7 +462,7 @@ impl ApplicationHandler<WakerEvent> for App {
} }
} }
if !consumed { 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(); let animating = self.is_animating();
@ -558,12 +490,10 @@ impl ApplicationHandler<WakerEvent> for App {
now - self.t now - self.t
); );
self.t = now; 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 { let Some(window) = self.windows.values().next() else {
return; return;
}; };

View 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()
}

View file

@ -31,9 +31,10 @@ pub fn main() {
let event_loop = EventsLoop::new(opts.headless, opts.output_file.is_some()) let event_loop = EventsLoop::new(opts.headless, opts.output_file.is_some())
.expect("Failed to create events loop"); .expect("Failed to create events loop");
let mut app = App::new(opts, preferences, servoshell_preferences, &event_loop); {
let mut app = App::new(opts, preferences, servoshell_preferences, &event_loop);
event_loop.run_app(&mut app); event_loop.run_app(&mut app);
}
crate::platform::deinit(clean_shutdown) crate::platform::deinit(clean_shutdown)
} }

View file

@ -92,4 +92,8 @@ impl Dialog {
}, },
} }
} }
pub(crate) fn is_file_dialog(&self) -> bool {
matches!(self, Dialog::File(..))
}
} }

View file

@ -93,7 +93,7 @@ impl EventsLoop {
app.init(None); app.init(None);
loop { loop {
self.sleep(flag, condvar); self.sleep(flag, condvar);
if app.handle_events_with_headless() { if !app.handle_events_with_headless() {
break; break;
} }
if !app.is_animating() { if !app.is_animating() {

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

View file

@ -10,7 +10,6 @@ use std::env;
use std::rc::Rc; use std::rc::Rc;
use std::time::Duration; use std::time::Duration;
use arboard::Clipboard;
use euclid::{Angle, Length, Point2D, Rotation3D, Scale, Size2D, UnknownUnit, Vector2D, Vector3D}; use euclid::{Angle, Length, Point2D, Rotation3D, Scale, Size2D, UnknownUnit, Vector2D, Vector3D};
use keyboard_types::{Modifiers, ShortcutMatcher}; use keyboard_types::{Modifiers, ShortcutMatcher};
use log::{debug, info}; use log::{debug, info};
@ -26,7 +25,7 @@ use servo::webrender_api::ScrollLocation;
use servo::webrender_traits::SurfmanRenderingContext; use servo::webrender_traits::SurfmanRenderingContext;
use servo::{ use servo::{
ClipboardEventType, Cursor, Key, KeyState, KeyboardEvent, MouseButton as ServoMouseButton, 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 surfman::{Context, Device, SurfaceType};
use url::Url; use url::Url;
@ -37,9 +36,9 @@ use winit::keyboard::{Key as LogicalKey, ModifiersState, NamedKey};
#[cfg(any(target_os = "linux", target_os = "windows"))] #[cfg(any(target_os = "linux", target_os = "windows"))]
use winit::window::Icon; use winit::window::Icon;
use super::app_state::RunningAppState;
use super::geometry::{winit_position_to_euclid_point, winit_size_to_euclid_size}; 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::keyutils::{keyboard_event_from_winit, CMD_OR_ALT};
use super::webview::{WebView as ServoShellWebView, WebViewManager};
use super::window_trait::{WindowPortsMethods, LINE_HEIGHT}; use super::window_trait::{WindowPortsMethods, LINE_HEIGHT};
use crate::desktop::keyutils::CMD_OR_CONTROL; use crate::desktop::keyutils::CMD_OR_CONTROL;
@ -195,25 +194,16 @@ impl Window {
webview.notify_keyboard_event(event); webview.notify_keyboard_event(event);
} }
fn handle_keyboard_input( fn handle_keyboard_input(&self, state: Rc<RunningAppState>, winit_event: KeyEvent) {
&self,
servo: &Servo,
clipboard: &mut Option<Clipboard>,
webviews: &mut WebViewManager,
winit_event: KeyEvent,
) {
// First, handle servoshell key bindings that are not overridable by, or visible to, the page. // First, handle servoshell key bindings that are not overridable by, or visible to, the page.
let mut keyboard_event = let mut keyboard_event =
keyboard_event_from_winit(&winit_event, self.modifiers_state.get()); 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; return;
} }
// Then we deliver character and keyboard events to the page in the focused webview. // Then we deliver character and keyboard events to the page in the focused webview.
let Some(webview) = webviews let Some(webview) = state.focused_webview() else {
.focused_webview()
.map(|webview| webview.servo_webview.clone())
else {
return; return;
}; };
@ -296,15 +286,10 @@ impl Window {
/// Handle key events before sending them to Servo. /// Handle key events before sending them to Servo.
fn handle_intercepted_key_bindings( fn handle_intercepted_key_bindings(
&self, &self,
servo: &Servo, state: Rc<RunningAppState>,
clipboard: &mut Option<Clipboard>,
webviews: &mut WebViewManager,
key_event: &KeyboardEvent, key_event: &KeyboardEvent,
) -> bool { ) -> bool {
let Some(focused_webview) = webviews let Some(focused_webview) = state.focused_webview() else {
.focused_webview()
.map(|webview| webview.servo_webview.clone())
else {
return false; return false;
}; };
@ -312,7 +297,7 @@ impl Window {
ShortcutMatcher::from_event(key_event.clone()) ShortcutMatcher::from_event(key_event.clone())
.shortcut(CMD_OR_CONTROL, 'R', || focused_webview.reload()) .shortcut(CMD_OR_CONTROL, 'R', || focused_webview.reload())
.shortcut(CMD_OR_CONTROL, 'W', || { .shortcut(CMD_OR_CONTROL, 'W', || {
webviews.close_webview(servo, focused_webview.id()); state.close_webview(focused_webview.id());
}) })
.shortcut(CMD_OR_CONTROL, 'P', || { .shortcut(CMD_OR_CONTROL, 'P', || {
let rate = env::var("SAMPLING_RATE") let rate = env::var("SAMPLING_RATE")
@ -335,7 +320,9 @@ impl Window {
focused_webview.notify_clipboard_event(ClipboardEventType::Copy); focused_webview.notify_clipboard_event(ClipboardEventType::Copy);
}) })
.shortcut(CMD_OR_CONTROL, 'V', || { .shortcut(CMD_OR_CONTROL, 'V', || {
let text = clipboard let text = state
.inner_mut()
.clipboard
.as_mut() .as_mut()
.and_then(|clipboard| clipboard.get_text().ok()) .and_then(|clipboard| clipboard.get_text().ok())
.unwrap_or_default(); .unwrap_or_default();
@ -382,40 +369,40 @@ impl Window {
|| focused_webview.exit_fullscreen(), || focused_webview.exit_fullscreen(),
) )
// Select the first 8 tabs via shortcuts // Select the first 8 tabs via shortcuts
.shortcut(CMD_OR_CONTROL, '1', || webviews.focus_webview_by_index(0)) .shortcut(CMD_OR_CONTROL, '1', || state.focus_webview_by_index(0))
.shortcut(CMD_OR_CONTROL, '2', || webviews.focus_webview_by_index(1)) .shortcut(CMD_OR_CONTROL, '2', || state.focus_webview_by_index(1))
.shortcut(CMD_OR_CONTROL, '3', || webviews.focus_webview_by_index(2)) .shortcut(CMD_OR_CONTROL, '3', || state.focus_webview_by_index(2))
.shortcut(CMD_OR_CONTROL, '4', || webviews.focus_webview_by_index(3)) .shortcut(CMD_OR_CONTROL, '4', || state.focus_webview_by_index(3))
.shortcut(CMD_OR_CONTROL, '5', || webviews.focus_webview_by_index(4)) .shortcut(CMD_OR_CONTROL, '5', || state.focus_webview_by_index(4))
.shortcut(CMD_OR_CONTROL, '6', || webviews.focus_webview_by_index(5)) .shortcut(CMD_OR_CONTROL, '6', || state.focus_webview_by_index(5))
.shortcut(CMD_OR_CONTROL, '7', || webviews.focus_webview_by_index(6)) .shortcut(CMD_OR_CONTROL, '7', || state.focus_webview_by_index(6))
.shortcut(CMD_OR_CONTROL, '8', || webviews.focus_webview_by_index(7)) .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 // Cmd/Ctrl 9 is a bit different in that it focuses the last tab instead of the 9th
.shortcut(CMD_OR_CONTROL, '9', || { .shortcut(CMD_OR_CONTROL, '9', || {
let len = webviews.webviews().len(); let len = state.webviews().len();
if len > 0 { if len > 0 {
webviews.focus_webview_by_index(len - 1) state.focus_webview_by_index(len - 1)
} }
}) })
.shortcut(Modifiers::CONTROL, Key::PageDown, || { .shortcut(Modifiers::CONTROL, Key::PageDown, || {
if let Some(index) = webviews.get_focused_webview_index() { if let Some(index) = state.get_focused_webview_index() {
webviews.focus_webview_by_index((index + 1) % webviews.webviews().len()) state.focus_webview_by_index((index + 1) % state.webviews().len())
} }
}) })
.shortcut(Modifiers::CONTROL, Key::PageUp, || { .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 { let new_index = if index == 0 {
webviews.webviews().len() - 1 state.webviews().len() - 1
} else { } else {
index - 1 index - 1
}; };
webviews.focus_webview_by_index(new_index) state.focus_webview_by_index(new_index)
} }
}) })
.shortcut(CMD_OR_CONTROL, 'T', || { .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); .otherwise(|| handled = false);
handled handled
} }
@ -442,7 +429,7 @@ impl WindowPortsMethods for Window {
self.winit_window.set_title(title); 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 = self.toolbar_height() * self.hidpi_factor();
let toolbar_height = toolbar_height.get().ceil() as i32; let toolbar_height = toolbar_height.get().ceil() as i32;
let total_size = PhysicalSize::new(size.width, size.height + toolbar_height); let total_size = PhysicalSize::new(size.width, size.height + toolbar_height);
@ -536,23 +523,14 @@ impl WindowPortsMethods for Window {
self.winit_window.id() self.winit_window.id()
} }
fn handle_winit_event( fn handle_winit_event(&self, state: Rc<RunningAppState>, event: winit::event::WindowEvent) {
&self, let Some(webview) = state.focused_webview() else {
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 {
return; return;
}; };
match event { match event {
winit::event::WindowEvent::KeyboardInput { 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) => { winit::event::WindowEvent::ModifiersChanged(modifiers) => {
self.modifiers_state.set(modifiers.state()) self.modifiers_state.set(modifiers.state())
@ -615,7 +593,7 @@ impl WindowPortsMethods for Window {
webview.set_pinch_zoom(delta as f32 + 1.0); webview.set_pinch_zoom(delta as f32 + 1.0);
}, },
winit::event::WindowEvent::CloseRequested => { winit::event::WindowEvent::CloseRequested => {
servo.start_shutting_down(); state.servo().start_shutting_down();
}, },
winit::event::WindowEvent::Resized(new_size) => { winit::event::WindowEvent::Resized(new_size) => {
if self.inner_size.get() != new_size { if self.inner_size.get() != new_size {

View file

@ -7,15 +7,13 @@
use std::cell::Cell; use std::cell::Cell;
use std::rc::Rc; use std::rc::Rc;
use arboard::Clipboard;
use euclid::num::Zero; use euclid::num::Zero;
use euclid::{Box2D, Length, Point2D, Scale, Size2D}; use euclid::{Box2D, Length, Point2D, Scale, Size2D};
use servo::compositing::windowing::{AnimationState, EmbedderCoordinates, WindowMethods}; use servo::compositing::windowing::{AnimationState, EmbedderCoordinates, WindowMethods};
use servo::servo_geometry::DeviceIndependentPixel; use servo::servo_geometry::DeviceIndependentPixel;
use servo::webrender_api::units::{DeviceIntSize, DevicePixel}; 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; use crate::desktop::window_trait::WindowPortsMethods;
pub struct Window { pub struct Window {
@ -58,10 +56,6 @@ impl Window {
Rc::new(window) Rc::new(window)
} }
pub fn new_uninit() -> Rc<dyn WindowPortsMethods> {
Self::new(Default::default(), None, None)
}
} }
impl WindowPortsMethods for Window { impl WindowPortsMethods for Window {
@ -69,7 +63,11 @@ impl WindowPortsMethods for Window {
winit::window::WindowId::dummy() 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. // Surfman doesn't support zero-sized surfaces.
let new_size = DeviceIntSize::new(size.width.max(1), size.height.max(1)); let new_size = DeviceIntSize::new(size.width.max(1), size.height.max(1));
if self.inner_size.get() == new_size { 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 // 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 // notification (such as from the display manager) that it has changed size, so we
// must notify the compositor here. // must notify the compositor here.
webview.servo_webview.notify_rendering_context_resized(); webview.notify_rendering_context_resized();
Some(new_size) Some(new_size)
} }
@ -114,13 +112,7 @@ impl WindowPortsMethods for Window {
self.animation_state.get() == AnimationState::Animating self.animation_state.get() == AnimationState::Animating
} }
fn handle_winit_event( fn handle_winit_event(&self, _: Rc<RunningAppState>, _: winit::event::WindowEvent) {
&self,
_: &Servo,
_: &mut Option<Clipboard>,
_: &mut WebViewManager,
_: winit::event::WindowEvent,
) {
// Not expecting any winit events. // Not expecting any winit events.
} }

View file

@ -24,14 +24,14 @@ use servo::servo_geometry::DeviceIndependentPixel;
use servo::servo_url::ServoUrl; use servo::servo_url::ServoUrl;
use servo::webrender_api::units::DevicePixel; use servo::webrender_api::units::DevicePixel;
use servo::webrender_traits::SurfmanRenderingContext; use servo::webrender_traits::SurfmanRenderingContext;
use servo::{LoadStatus, Servo}; use servo::{LoadStatus, WebView};
use winit::event::{ElementState, MouseButton, WindowEvent}; use winit::event::{ElementState, MouseButton, WindowEvent};
use winit::event_loop::ActiveEventLoop; use winit::event_loop::ActiveEventLoop;
use winit::window::Window; use winit::window::Window;
use super::app_state::RunningAppState;
use super::egui_glue::EguiGlow; use super::egui_glue::EguiGlow;
use super::geometry::winit_position_to_euclid_point; use super::geometry::winit_position_to_euclid_point;
use super::webview::{WebView, WebViewManager};
pub struct Minibrowser { pub struct Minibrowser {
pub context: EguiGlow, 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 { impl Minibrowser {
pub fn new( pub fn new(
rendering_context: &SurfmanRenderingContext, rendering_context: &SurfmanRenderingContext,
@ -173,12 +179,13 @@ impl Minibrowser {
/// Draws a browser tab, checking for clicks and queues appropriate `MinibrowserEvent`s. /// 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 /// 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. /// supports that, so we arrange multiple Widgets in a way that they look connected.
fn browser_tab( fn browser_tab(ui: &mut egui::Ui, webview: WebView, event_queue: &mut Vec<MinibrowserEvent>) {
ui: &mut egui::Ui, let label = match (webview.page_title(), webview.url()) {
label: &str, (Some(title), _) if !title.is_empty() => title,
webview: &WebView, (_, Some(url)) => url.to_string(),
event_queue: &mut Vec<MinibrowserEvent>, _ => "New Tab".into(),
) { };
let old_item_spacing = ui.spacing().item_spacing; let old_item_spacing = ui.spacing().item_spacing;
let old_visuals = ui.visuals().clone(); let old_visuals = ui.visuals().clone();
let active_bg_color = old_visuals.widgets.active.weak_bg_fill; let active_bg_color = old_visuals.widgets.active.weak_bg_fill;
@ -214,10 +221,10 @@ impl Minibrowser {
visuals.widgets.hovered.rounding = rounding; visuals.widgets.hovered.rounding = rounding;
visuals.widgets.inactive.rounding = rounding; visuals.widgets.inactive.rounding = rounding;
let selected = webview.focused; let selected = webview.focused();
let tab = ui.add(SelectableLabel::new( let tab = ui.add(SelectableLabel::new(
selected, selected,
truncate_with_ellipsis(label, 20), truncate_with_ellipsis(&label, 20),
)); ));
let tab = tab.on_hover_ui(|ui| { let tab = tab.on_hover_ui(|ui| {
ui.label(label); ui.label(label);
@ -244,22 +251,16 @@ impl Minibrowser {
let close_button = ui.add(egui::Button::new("X").fill(fill_color)); let close_button = ui.add(egui::Button::new("X").fill(fill_color));
*ui.visuals_mut() = old_visuals; *ui.visuals_mut() = old_visuals;
if close_button.clicked() || close_button.middle_clicked() || tab.middle_clicked() { 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() { } else if !selected && tab.clicked() {
webview.servo_webview.focus(); webview.focus();
} }
} }
/// Update the minibrowser, but dont paint. /// Update the minibrowser, but dont paint.
/// If `servo_framebuffer_id` is given, set up a paint callback to blit its contents to our /// If `servo_framebuffer_id` is given, set up a paint callback to blit its contents to our
/// CentralPanel when [`Minibrowser::paint`] is called. /// CentralPanel when [`Minibrowser::paint`] is called.
pub fn update( pub fn update(&mut self, window: &Window, state: &RunningAppState, reason: &'static str) {
&mut self,
window: &Window,
webviews: &mut WebViewManager,
servo: Option<&Servo>,
reason: &'static str,
) {
let now = Instant::now(); let now = Instant::now();
trace!( trace!(
"{:?} since last update ({})", "{:?} since last update ({})",
@ -277,9 +278,7 @@ impl Minibrowser {
.. ..
} = self; } = self;
let widget_fbo = *widget_surface_fbo; let widget_fbo = *widget_surface_fbo;
let servo_framebuffer_id = servo let servo_framebuffer_id = state.servo().offscreen_framebuffer_id();
.as_ref()
.and_then(|servo| servo.offscreen_framebuffer_id());
let _duration = context.run(window, |ctx| { let _duration = context.run(window, |ctx| {
// TODO: While in fullscreen add some way to mitigate the increased phishing risk // 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 // when not displaying the URL bar: https://github.com/servo/servo/issues/32443
@ -362,13 +361,8 @@ impl Minibrowser {
ui.available_size(), ui.available_size(),
egui::Layout::left_to_right(egui::Align::Center), egui::Layout::left_to_right(egui::Align::Center),
|ui| { |ui| {
for (_, webview) in webviews.webviews().into_iter() { for (_, webview) in state.webviews().into_iter() {
let label = match (&webview.title, &webview.url) { Self::browser_tab(ui, webview, &mut event_queue.borrow_mut());
(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());
} }
if ui.add(Minibrowser::toolbar_button("+")).clicked() { if ui.add(Minibrowser::toolbar_button("+")).clicked() {
event_queue.borrow_mut().push(MinibrowserEvent::NewWebView); event_queue.borrow_mut().push(MinibrowserEvent::NewWebView);
@ -384,17 +378,14 @@ impl Minibrowser {
let scale = let scale =
Scale::<_, DeviceIndependentPixel, DevicePixel>::new(ctx.pixels_per_point()); 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, |_| { 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() CentralPanel::default()
.frame(Frame::none()) .frame(Frame::none())
.show(ctx, |ui| { .show(ctx, |ui| {
@ -407,9 +398,8 @@ impl Minibrowser {
Point2D::new(x, y), Point2D::new(x, y),
Size2D::new(width, height), Size2D::new(width, height),
) * scale; ) * scale;
if rect != webview.rect { if rect != webview.rect() {
webview.rect = rect; webview.move_resize(rect);
webview.servo_webview.move_resize(rect)
} }
let min = ui.cursor().min; let min = ui.cursor().min;
let size = ui.available_size(); let size = ui.available_size();
@ -489,13 +479,16 @@ impl Minibrowser {
/// Updates the location field from the given [WebViewManager], unless the user has started /// 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). /// 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? // User edited without clicking Go?
if self.location_dirty.get() { if self.location_dirty.get() {
return false; 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() => { Some(location) if location != *self.location.get_mut() => {
self.location = RefCell::new(location.to_owned()); self.location = RefCell::new(location.to_owned());
true true
@ -508,29 +501,32 @@ impl Minibrowser {
self.location_dirty.set(dirty); self.location_dirty.set(dirty);
} }
/// Updates the spinner from the given [WebViewManager], returning true iff it has changed pub fn update_load_status(&mut self, state: &RunningAppState) -> bool {
/// (needing an egui update). let state_status = state
pub fn update_spinner_in_toolbar(&mut self, browser: &mut WebViewManager) -> bool { .focused_webview()
let need_update = browser.load_status() != self.load_status; .map(|webview| webview.load_status())
self.load_status = browser.load_status(); .unwrap_or(LoadStatus::Complete);
need_update 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 { pub fn update_status_text(&mut self, state: &RunningAppState) -> bool {
let need_update = browser.status_text() != self.status_text; let state_status = state
self.status_text = browser.status_text(); .focused_webview()
need_update .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. /// Updates all fields taken from the given [WebViewManager], such as the location field.
/// Returns true iff the egui needs an update. /// 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" (||) // 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. // 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 // We want to ensure that all functions are called. The "bitwise OR" operator
// does not short-circuit. // does not short-circuit.
self.update_location_in_toolbar(browser) | self.update_location_in_toolbar(state) |
self.update_spinner_in_toolbar(browser) | self.update_load_status(state) |
self.update_status_text(browser) self.update_status_text(state)
} }
} }

View file

@ -5,11 +5,13 @@
//! Contains files specific to the servoshell app for Desktop systems. //! Contains files specific to the servoshell app for Desktop systems.
pub(crate) mod app; pub(crate) mod app;
mod app_state;
pub(crate) mod cli; pub(crate) mod cli;
mod dialog; mod dialog;
mod egui_glue; mod egui_glue;
mod embedder; mod embedder;
pub(crate) mod events_loop; pub(crate) mod events_loop;
mod gamepad;
pub mod geometry; pub mod geometry;
mod headed_window; mod headed_window;
mod headless_window; mod headless_window;
@ -17,5 +19,4 @@ mod keyutils;
mod minibrowser; mod minibrowser;
mod protocols; mod protocols;
mod tracing; mod tracing;
mod webview;
mod window_trait; mod window_trait;

View file

@ -22,21 +22,7 @@ macro_rules! trace_winit_event {
}; };
} }
/// Log an event from servo ([servo::EmbedderMsg]) at trace level. pub(crate) use trace_winit_event;
/// - 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};
/// Get the log target for an event, as a static string. /// Get the log target for an event, as a static string.
pub(crate) trait LogTarget { 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"),
}
}
}
}

View file

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

View file

@ -7,14 +7,13 @@
use std::rc::Rc; use std::rc::Rc;
use arboard::Clipboard;
use euclid::{Length, Scale}; use euclid::{Length, Scale};
use servo::compositing::windowing::WindowMethods; use servo::compositing::windowing::WindowMethods;
use servo::servo_geometry::DeviceIndependentPixel; use servo::servo_geometry::DeviceIndependentPixel;
use servo::webrender_api::units::{DeviceIntPoint, DeviceIntSize, DevicePixel}; 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) // This should vary by zoom level and maybe actual text size (focused or under cursor)
pub const LINE_HEIGHT: f32 = 38.0; pub const LINE_HEIGHT: f32 = 38.0;
@ -31,13 +30,7 @@ pub trait WindowPortsMethods: WindowMethods {
) -> Option<Scale<f32, DeviceIndependentPixel, DevicePixel>>; ) -> Option<Scale<f32, DeviceIndependentPixel, DevicePixel>>;
fn page_height(&self) -> f32; fn page_height(&self) -> f32;
fn get_fullscreen(&self) -> bool; fn get_fullscreen(&self) -> bool;
fn handle_winit_event( fn handle_winit_event(&self, state: Rc<RunningAppState>, event: winit::event::WindowEvent);
&self,
servo: &Servo,
clipboard: &mut Option<Clipboard>,
webviews: &mut WebViewManager,
event: winit::event::WindowEvent,
);
fn is_animating(&self) -> bool; fn is_animating(&self) -> bool;
fn set_title(&self, _title: &str) {} fn set_title(&self, _title: &str) {}
/// Request a new inner size for the window, not including external decorations. /// Request a new inner size for the window, not including external decorations.