From 5f08e4fa76113625ce5592042008c5cdc9536e3e Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Thu, 6 Feb 2025 08:33:31 +0100 Subject: [PATCH] servoshell: Port desktop servoshell to use delegate API (#35284) Signed-off-by: Martin Robinson Co-authored-by: Delan Azabani Co-authored-by: Mukilan Thiyagarajan --- components/servo/lib.rs | 6 +- components/servo/webview_delegate.rs | 8 +- ports/servoshell/desktop/app.rs | 224 ++--- ports/servoshell/desktop/app_state.rs | 686 +++++++++++++++ ports/servoshell/desktop/cli.rs | 7 +- ports/servoshell/desktop/dialog.rs | 4 + ports/servoshell/desktop/events_loop.rs | 2 +- ports/servoshell/desktop/gamepad.rs | 230 +++++ ports/servoshell/desktop/headed_window.rs | 88 +- ports/servoshell/desktop/headless_window.rs | 24 +- ports/servoshell/desktop/minibrowser.rs | 106 ++- ports/servoshell/desktop/mod.rs | 3 +- ports/servoshell/desktop/tracing.rs | 71 +- ports/servoshell/desktop/webview.rs | 895 -------------------- ports/servoshell/desktop/window_trait.rs | 13 +- 15 files changed, 1109 insertions(+), 1258 deletions(-) create mode 100644 ports/servoshell/desktop/app_state.rs create mode 100644 ports/servoshell/desktop/gamepad.rs delete mode 100644 ports/servoshell/desktop/webview.rs diff --git a/components/servo/lib.rs b/components/servo/lib.rs index f2928ed93ce..886efc32991 100644 --- a/components/servo/lib.rs +++ b/components/servo/lib.rs @@ -1089,7 +1089,11 @@ impl Servo { webview.delegate().notify_ready_to_show(webview); } }, - EmbedderMsg::WebViewClosed(_) => {}, + EmbedderMsg::WebViewClosed(webview_id) => { + if let Some(webview) = self.get_webview_handle(webview_id) { + webview.delegate().notify_closed(webview); + } + }, EmbedderMsg::WebViewFocused(webview_id) => { for id in self.webviews.borrow().keys() { if let Some(webview) = self.get_webview_handle(*id) { diff --git a/components/servo/webview_delegate.rs b/components/servo/webview_delegate.rs index 5648294b2c6..846c62d7439 100644 --- a/components/servo/webview_delegate.rs +++ b/components/servo/webview_delegate.rs @@ -117,6 +117,10 @@ pub trait WebViewDelegate { /// The history state has changed. // changed pattern; maybe wasteful if embedder doesn’t care? fn notify_history_changed(&self, _webview: WebView, _: Vec, _: usize) {} + /// Page content has closed this [`WebView`] via `window.close()`. It's the embedder's + /// responsibility to remove the [`WebView`] from the interface when this notification + /// occurs. + fn notify_closed(&self, _webview: WebView) {} /// A keyboard event has been sent to Servo, but remains unprocessed. This allows the /// embedding application to handle key events while first letting the [`WebView`] @@ -144,10 +148,6 @@ pub trait WebViewDelegate { fn request_open_auxiliary_webview(&self, _parent_webview: WebView) -> Option { None } - /// Page content has requested that this [`WebView`] be closed. It's the embedder's - /// responsibility to either ignore this request or to remove the [`WebView`] from the - /// interface. - fn request_close(&self, _webview: WebView) {} /// Open interface to request permission specified by prompt. fn request_permission( &self, diff --git a/ports/servoshell/desktop/app.rs b/ports/servoshell/desktop/app.rs index 1457bc5dc99..2712e796b4e 100644 --- a/ports/servoshell/desktop/app.rs +++ b/ports/servoshell/desktop/app.rs @@ -10,7 +10,6 @@ use std::rc::Rc; use std::time::Instant; use std::{env, fs}; -use arboard::Clipboard; use log::{info, trace, warn}; use raw_window_handle::HasDisplayHandle; use servo::compositing::windowing::{AnimationState, WindowMethods}; @@ -31,10 +30,11 @@ use winit::event::WindowEvent; use winit::event_loop::{ActiveEventLoop, ControlFlow}; use winit::window::WindowId; +use super::app_state::AppState; use super::events_loop::{EventsLoop, WakerEvent}; use super::minibrowser::{Minibrowser, MinibrowserEvent}; -use super::webview::WebViewManager; use super::{headed_window, headless_window}; +use crate::desktop::app_state::RunningAppState; use crate::desktop::embedder::{EmbedderCallbacks, XrDiscovery}; use crate::desktop::tracing::trace_winit_event; use crate::desktop::window_trait::WindowPortsMethods; @@ -45,9 +45,6 @@ pub struct App { opts: Opts, preferences: Preferences, servo_shell_preferences: ServoShellPreferences, - clipboard: Option, - servo: Option, - webviews: WebViewManager, suspended: Cell, windows: HashMap>, minibrowser: Option, @@ -55,15 +52,16 @@ pub struct App { initial_url: ServoUrl, t_start: Instant, t: Instant, + state: AppState, } -enum Present { +pub(crate) enum Present { Deferred, None, } /// Action to be taken by the caller of [`App::handle_events`]. -enum PumpResult { +pub(crate) enum PumpResult { /// The caller should shut down Servo and its related context. Shutdown, Continue { @@ -91,9 +89,6 @@ impl App { opts, preferences, servo_shell_preferences, - clipboard: Clipboard::new().ok(), - webviews: WebViewManager::new(), - servo: None, suspended: Cell::new(false), windows: HashMap::new(), minibrowser: None, @@ -101,6 +96,7 @@ impl App { initial_url: initial_url.clone(), t_start: t, t, + state: AppState::Initializing, } } @@ -149,8 +145,6 @@ impl App { )) }; - // Create window's context - self.webviews.set_window(window.clone()); if window.winit_window().is_some() { self.minibrowser = Some(Minibrowser::new( &rendering_context, @@ -159,17 +153,6 @@ impl App { )); } - if let Some(ref mut minibrowser) = self.minibrowser { - // Servo is not yet initialised, so there is no `servo_framebuffer_id`. - minibrowser.update( - window.winit_window().unwrap(), - &mut self.webviews, - self.servo.as_ref(), - "init", - ); - window.set_toolbar_height(minibrowser.toolbar_height); - } - self.windows.insert(window.id(), window); self.suspended.set(false); @@ -192,6 +175,15 @@ impl App { None }; + // Implements embedder methods, used by libservo and constellation. + let embedder = Box::new(EmbedderCallbacks::new(self.waker.clone(), xr_discovery)); + + let composite_target = if self.minibrowser.is_some() { + CompositeTarget::OffscreenFbo + } else { + CompositeTarget::ContextFbo + }; + // TODO: Remove this once dyn upcasting coercion stabilises // struct UpcastedWindow(Rc); @@ -203,116 +195,61 @@ impl App { self.0.set_animation_state(state); } } - let window = UpcastedWindow(window.clone()); - // Implements embedder methods, used by libservo and constellation. - let embedder = Box::new(EmbedderCallbacks::new(self.waker.clone(), xr_discovery)); - let composite_target = if self.minibrowser.is_some() { - CompositeTarget::OffscreenFbo - } else { - CompositeTarget::ContextFbo - }; let servo = Servo::new( self.opts.clone(), self.preferences.clone(), Rc::new(rendering_context), embedder, - Rc::new(window), + Rc::new(UpcastedWindow(window.clone())), self.servo_shell_preferences.user_agent.clone(), composite_target, ); - servo.setup_logging(); - let webview = servo.new_webview(self.initial_url.clone().into_url()); - self.webviews.add(webview); + let running_state = Rc::new(RunningAppState::new( + servo, + window.clone(), + self.opts.headless, + )); + running_state.new_toplevel_webview(self.initial_url.clone().into_url()); - self.servo = Some(servo); + if let Some(ref mut minibrowser) = self.minibrowser { + minibrowser.update(window.winit_window().unwrap(), &running_state, "init"); + window.set_toolbar_height(minibrowser.toolbar_height); + } + + self.state = AppState::Running(running_state); } pub fn is_animating(&self) -> bool { self.windows.iter().any(|(_, window)| window.is_animating()) } - /// Spins the Servo event loop, and (for now) handles a few other tasks: - /// - Notifying Servo about incoming gamepad events - /// - Receiving updates from Servo - /// - Performing updates in the compositor, such as queued pinch zoom events - /// - /// In the future, these tasks may be decoupled. - fn handle_events(&mut self) -> PumpResult { - // If the Gamepad API is enabled, handle gamepad events from GilRs. - // Checking for focused_webview_id should ensure we'll have a valid browsing context. - if pref!(dom_gamepad_enabled) && self.webviews.focused_webview_id().is_some() { - self.webviews.handle_gamepad_events(); - } - - // Take any new embedder messages from Servo. - let servo = self.servo.as_mut().expect("Servo should be running."); - let mut embedder_messages = servo.get_events(); - let mut need_present = false; - let mut need_update = false; - loop { - // Consume and handle those embedder messages. - let servo_event_response = self.webviews.handle_servo_events( - servo, - &mut self.clipboard, - &self.opts, - embedder_messages, - ); - need_present |= servo_event_response.need_present; - need_update |= servo_event_response.need_update; - - // Runs the compositor, and receives and collects embedder messages from various Servo components. - servo.handle_events(vec![]); - - if self.webviews.shutdown_requested() { - return PumpResult::Shutdown; - } - - // Take any new embedder messages from Servo itself. - embedder_messages = servo.get_events(); - if embedder_messages.is_empty() { - break; - } - } - - let present = if need_present { - Present::Deferred - } else { - Present::None - }; - - PumpResult::Continue { - update: need_update, - present, - } - } - /// Handle events with winit contexts pub fn handle_events_with_winit( &mut self, event_loop: &ActiveEventLoop, window: Rc, ) { - match self.handle_events() { + let AppState::Running(state) = &self.state else { + return; + }; + + match state.pump_event_loop() { PumpResult::Shutdown => { - event_loop.exit(); - self.servo.take().unwrap().deinit(); - if let Some(ref mut minibrowser) = self.minibrowser { - minibrowser.context.destroy(); - } + state.shutdown(); + self.state = AppState::ShuttingDown; }, PumpResult::Continue { update, present } => { if update { if let Some(ref mut minibrowser) = self.minibrowser { - if minibrowser.update_webview_data(&mut self.webviews) { + if minibrowser.update_webview_data(state) { // Update the minibrowser immediately. While we could update by requesting a // redraw, doing so would delay the location update by two frames. minibrowser.update( window.winit_window().unwrap(), - &mut self.webviews, - self.servo.as_ref(), + state, "update_location_in_toolbar", ); } @@ -327,16 +264,21 @@ impl App { if let Some(window) = window.winit_window() { window.request_redraw(); } else { - self.servo.as_mut().unwrap().present(); + state.servo().present(); } }, Present::None => {}, } }, } + + if matches!(self.state, AppState::ShuttingDown) { + event_loop.exit(); + } } - /// Handle all servo events with headless mode. Return true if servo request to shutdown. + /// Handle all servo events with headless mode. Return true if the application should + /// continue. pub fn handle_events_with_headless(&mut self) -> bool { let now = Instant::now(); let event = winit::event::Event::UserEvent(WakerEvent); @@ -347,20 +289,16 @@ impl App { now - self.t ); self.t = now; - // If self.servo is None here, it means that we're in the process of shutting down, - // let's ignore events. - if self.servo.is_none() { - return false; - } - let mut exit = false; - match self.handle_events() { + // We should always be in the running state. + let AppState::Running(state) = &self.state else { + return false; + }; + + match state.pump_event_loop() { PumpResult::Shutdown => { - exit = true; - self.servo.take().unwrap().deinit(); - if let Some(ref mut minibrowser) = self.minibrowser { - minibrowser.context.destroy(); - } + state.shutdown(); + self.state = AppState::ShuttingDown; }, PumpResult::Continue { present, .. } => { match present { @@ -368,13 +306,14 @@ impl App { // The compositor has painted to this frame. trace!("PumpResult::Present::Deferred"); // In headless mode, we present directly. - self.servo.as_mut().unwrap().present(); + state.servo().present(); }, Present::None => {}, } }, } - exit + + !matches!(self.state, AppState::ShuttingDown) } /// Takes any events generated during `egui` updates and performs their actions. @@ -382,7 +321,8 @@ impl App { let Some(minibrowser) = self.minibrowser.as_ref() else { return; }; - let Some(servo) = self.servo.as_ref() else { + // We should always be in the running state. + let AppState::Running(state) = &self.state else { return; }; @@ -397,34 +337,33 @@ impl App { warn!("failed to parse location"); break; }; - if let Some(focused_webview) = self.webviews.focused_webview() { - focused_webview.servo_webview.load(url.into_url()); + if let Some(focused_webview) = state.focused_webview() { + focused_webview.load(url.into_url()); } }, MinibrowserEvent::Back => { - if let Some(focused_webview) = self.webviews.focused_webview() { - focused_webview.servo_webview.go_back(1); + if let Some(focused_webview) = state.focused_webview() { + focused_webview.go_back(1); } }, MinibrowserEvent::Forward => { - if let Some(focused_webview) = self.webviews.focused_webview() { - focused_webview.servo_webview.go_forward(1); + if let Some(focused_webview) = state.focused_webview() { + focused_webview.go_forward(1); } }, MinibrowserEvent::Reload => { minibrowser.update_location_dirty(false); - if let Some(focused_webview) = self.webviews.focused_webview() { - focused_webview.servo_webview.reload(); + if let Some(focused_webview) = state.focused_webview() { + focused_webview.reload(); } }, MinibrowserEvent::NewWebView => { minibrowser.update_location_dirty(false); - let webview = servo.new_webview(Url::parse("servo:newtab").unwrap()); - self.webviews.add(webview); + state.new_toplevel_webview(Url::parse("servo:newtab").unwrap()); }, MinibrowserEvent::CloseWebView(id) => { minibrowser.update_location_dirty(false); - self.webviews.close_webview(servo, id); + state.close_webview(id); }, } } @@ -450,17 +389,16 @@ impl ApplicationHandler for App { now - self.t ); self.t = now; - // If self.servo is None here, it means that we're in the process of shutting down, - // let's ignore events. - let Some(ref mut servo) = self.servo else { + + let AppState::Running(state) = &self.state else { return; }; let Some(window) = self.windows.get(&window_id) else { return; }; - let window = window.clone(); + let window = window.clone(); if event == winit::event::WindowEvent::RedrawRequested { // We need to redraw the window for some reason. trace!("RedrawRequested"); @@ -468,16 +406,11 @@ impl ApplicationHandler for App { // WARNING: do not defer painting or presenting to some later tick of the event // loop or servoshell may become unresponsive! (servo#30312) if let Some(ref mut minibrowser) = self.minibrowser { - minibrowser.update( - window.winit_window().unwrap(), - &mut self.webviews, - Some(servo), - "RedrawRequested", - ); + minibrowser.update(window.winit_window().unwrap(), state, "RedrawRequested"); minibrowser.paint(window.winit_window().unwrap()); } - servo.present(); + state.servo().present(); } // Handle the event @@ -512,8 +445,7 @@ impl ApplicationHandler for App { if let WindowEvent::Resized(_) = event { minibrowser.update( window.winit_window().unwrap(), - &mut self.webviews, - Some(servo), + state, "Sync WebView size with Window Resize event", ); } @@ -530,7 +462,7 @@ impl ApplicationHandler for App { } } if !consumed { - window.handle_winit_event(servo, &mut self.clipboard, &mut self.webviews, event); + window.handle_winit_event(state.clone(), event); } let animating = self.is_animating(); @@ -558,12 +490,10 @@ impl ApplicationHandler for App { now - self.t ); self.t = now; - // If self.servo is None here, it means that we're in the process of shutting down, - // let's ignore events. - if self.servo.is_none() { - return; - } + if !matches!(self.state, AppState::Running(_)) { + return; + }; let Some(window) = self.windows.values().next() else { return; }; diff --git a/ports/servoshell/desktop/app_state.rs b/ports/servoshell/desktop/app_state.rs new file mode 100644 index 00000000000..4c3650df836 --- /dev/null +++ b/ports/servoshell/desktop/app_state.rs @@ -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), + 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, +} + +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, + + /// List of top-level browsing contexts. + /// Modified by EmbedderMsg::WebViewOpened and EmbedderMsg::WebViewClosed, + /// and we exit if it ever becomes empty. + webviews: HashMap, + + /// The order in which the webviews were created. + creation_order: Vec, + + /// The webview that is currently focused. + /// Modified by EmbedderMsg::WebViewFocused and EmbedderMsg::WebViewBlurred. + focused_webview_id: Option, + + /// The current set of open dialogs. + dialogs: HashMap>, + + /// A handle to the Window that Servo is rendering in -- either headed or headless. + window: Rc, + + /// Gamepad support, which may be `None` if it failed to initialize. + gamepad_support: Option, + + /// 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, + 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, url: Url) { + let webview = self.servo().new_webview(url); + webview.set_delegate(self.clone()); + self.add(webview); + } + + pub(crate) fn inner(&self) -> Ref { + self.inner.borrow() + } + + pub(crate) fn inner_mut(&self) -> RefMut { + 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 { + 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 { + 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) { + self.inner_mut().need_update = true; + } + + fn notify_page_title_changed(&self, webview: servo::WebView, title: Option) { + 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 { + 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) { + 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, + response_sender: IpcSender>, + ) { + 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, + allow_select_mutiple: bool, + response_sender: IpcSender>>, + ) { + 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, + ) { + 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, + ) { + 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, + ) { + 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) -> Option { + 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) -> Option { + 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() +} diff --git a/ports/servoshell/desktop/cli.rs b/ports/servoshell/desktop/cli.rs index 1241e4e2af3..0e6c06368ba 100644 --- a/ports/servoshell/desktop/cli.rs +++ b/ports/servoshell/desktop/cli.rs @@ -31,9 +31,10 @@ pub fn main() { let event_loop = EventsLoop::new(opts.headless, opts.output_file.is_some()) .expect("Failed to create events loop"); - let mut app = App::new(opts, preferences, servoshell_preferences, &event_loop); - - event_loop.run_app(&mut app); + { + let mut app = App::new(opts, preferences, servoshell_preferences, &event_loop); + event_loop.run_app(&mut app); + } crate::platform::deinit(clean_shutdown) } diff --git a/ports/servoshell/desktop/dialog.rs b/ports/servoshell/desktop/dialog.rs index 650c65d22d7..e50893f45c7 100644 --- a/ports/servoshell/desktop/dialog.rs +++ b/ports/servoshell/desktop/dialog.rs @@ -92,4 +92,8 @@ impl Dialog { }, } } + + pub(crate) fn is_file_dialog(&self) -> bool { + matches!(self, Dialog::File(..)) + } } diff --git a/ports/servoshell/desktop/events_loop.rs b/ports/servoshell/desktop/events_loop.rs index 697594d1a0b..ffe40eb8cb9 100644 --- a/ports/servoshell/desktop/events_loop.rs +++ b/ports/servoshell/desktop/events_loop.rs @@ -93,7 +93,7 @@ impl EventsLoop { app.init(None); loop { self.sleep(flag, condvar); - if app.handle_events_with_headless() { + if !app.handle_events_with_headless() { break; } if !app.is_animating() { diff --git a/ports/servoshell/desktop/gamepad.rs b/ports/servoshell/desktop/gamepad.rs new file mode 100644 index 00000000000..e5c80722c4d --- /dev/null +++ b/ports/servoshell/desktop/gamepad.rs @@ -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, +} + +pub(crate) struct GamepadSupport { + handle: Gilrs, + haptic_effects: HashMap, +} + +impl GamepadSupport { + pub(crate) fn maybe_new() -> Option { + 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 = 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 + // + 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 + // + 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, + ) { + 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 + } +} diff --git a/ports/servoshell/desktop/headed_window.rs b/ports/servoshell/desktop/headed_window.rs index 38dea704497..4a103d0e38b 100644 --- a/ports/servoshell/desktop/headed_window.rs +++ b/ports/servoshell/desktop/headed_window.rs @@ -10,7 +10,6 @@ use std::env; use std::rc::Rc; use std::time::Duration; -use arboard::Clipboard; use euclid::{Angle, Length, Point2D, Rotation3D, Scale, Size2D, UnknownUnit, Vector2D, Vector3D}; use keyboard_types::{Modifiers, ShortcutMatcher}; use log::{debug, info}; @@ -26,7 +25,7 @@ use servo::webrender_api::ScrollLocation; use servo::webrender_traits::SurfmanRenderingContext; use servo::{ ClipboardEventType, Cursor, Key, KeyState, KeyboardEvent, MouseButton as ServoMouseButton, - Servo, Theme, TouchEventType, TouchId, WebView, WheelDelta, WheelMode, + Theme, TouchEventType, TouchId, WebView, WheelDelta, WheelMode, }; use surfman::{Context, Device, SurfaceType}; use url::Url; @@ -37,9 +36,9 @@ use winit::keyboard::{Key as LogicalKey, ModifiersState, NamedKey}; #[cfg(any(target_os = "linux", target_os = "windows"))] use winit::window::Icon; +use super::app_state::RunningAppState; use super::geometry::{winit_position_to_euclid_point, winit_size_to_euclid_size}; use super::keyutils::{keyboard_event_from_winit, CMD_OR_ALT}; -use super::webview::{WebView as ServoShellWebView, WebViewManager}; use super::window_trait::{WindowPortsMethods, LINE_HEIGHT}; use crate::desktop::keyutils::CMD_OR_CONTROL; @@ -195,25 +194,16 @@ impl Window { webview.notify_keyboard_event(event); } - fn handle_keyboard_input( - &self, - servo: &Servo, - clipboard: &mut Option, - webviews: &mut WebViewManager, - winit_event: KeyEvent, - ) { + fn handle_keyboard_input(&self, state: Rc, winit_event: KeyEvent) { // First, handle servoshell key bindings that are not overridable by, or visible to, the page. let mut keyboard_event = keyboard_event_from_winit(&winit_event, self.modifiers_state.get()); - if self.handle_intercepted_key_bindings(servo, clipboard, webviews, &keyboard_event) { + if self.handle_intercepted_key_bindings(state.clone(), &keyboard_event) { return; } // Then we deliver character and keyboard events to the page in the focused webview. - let Some(webview) = webviews - .focused_webview() - .map(|webview| webview.servo_webview.clone()) - else { + let Some(webview) = state.focused_webview() else { return; }; @@ -296,15 +286,10 @@ impl Window { /// Handle key events before sending them to Servo. fn handle_intercepted_key_bindings( &self, - servo: &Servo, - clipboard: &mut Option, - webviews: &mut WebViewManager, + state: Rc, key_event: &KeyboardEvent, ) -> bool { - let Some(focused_webview) = webviews - .focused_webview() - .map(|webview| webview.servo_webview.clone()) - else { + let Some(focused_webview) = state.focused_webview() else { return false; }; @@ -312,7 +297,7 @@ impl Window { ShortcutMatcher::from_event(key_event.clone()) .shortcut(CMD_OR_CONTROL, 'R', || focused_webview.reload()) .shortcut(CMD_OR_CONTROL, 'W', || { - webviews.close_webview(servo, focused_webview.id()); + state.close_webview(focused_webview.id()); }) .shortcut(CMD_OR_CONTROL, 'P', || { let rate = env::var("SAMPLING_RATE") @@ -335,7 +320,9 @@ impl Window { focused_webview.notify_clipboard_event(ClipboardEventType::Copy); }) .shortcut(CMD_OR_CONTROL, 'V', || { - let text = clipboard + let text = state + .inner_mut() + .clipboard .as_mut() .and_then(|clipboard| clipboard.get_text().ok()) .unwrap_or_default(); @@ -382,40 +369,40 @@ impl Window { || focused_webview.exit_fullscreen(), ) // Select the first 8 tabs via shortcuts - .shortcut(CMD_OR_CONTROL, '1', || webviews.focus_webview_by_index(0)) - .shortcut(CMD_OR_CONTROL, '2', || webviews.focus_webview_by_index(1)) - .shortcut(CMD_OR_CONTROL, '3', || webviews.focus_webview_by_index(2)) - .shortcut(CMD_OR_CONTROL, '4', || webviews.focus_webview_by_index(3)) - .shortcut(CMD_OR_CONTROL, '5', || webviews.focus_webview_by_index(4)) - .shortcut(CMD_OR_CONTROL, '6', || webviews.focus_webview_by_index(5)) - .shortcut(CMD_OR_CONTROL, '7', || webviews.focus_webview_by_index(6)) - .shortcut(CMD_OR_CONTROL, '8', || webviews.focus_webview_by_index(7)) + .shortcut(CMD_OR_CONTROL, '1', || state.focus_webview_by_index(0)) + .shortcut(CMD_OR_CONTROL, '2', || state.focus_webview_by_index(1)) + .shortcut(CMD_OR_CONTROL, '3', || state.focus_webview_by_index(2)) + .shortcut(CMD_OR_CONTROL, '4', || state.focus_webview_by_index(3)) + .shortcut(CMD_OR_CONTROL, '5', || state.focus_webview_by_index(4)) + .shortcut(CMD_OR_CONTROL, '6', || state.focus_webview_by_index(5)) + .shortcut(CMD_OR_CONTROL, '7', || state.focus_webview_by_index(6)) + .shortcut(CMD_OR_CONTROL, '8', || state.focus_webview_by_index(7)) // Cmd/Ctrl 9 is a bit different in that it focuses the last tab instead of the 9th .shortcut(CMD_OR_CONTROL, '9', || { - let len = webviews.webviews().len(); + let len = state.webviews().len(); if len > 0 { - webviews.focus_webview_by_index(len - 1) + state.focus_webview_by_index(len - 1) } }) .shortcut(Modifiers::CONTROL, Key::PageDown, || { - if let Some(index) = webviews.get_focused_webview_index() { - webviews.focus_webview_by_index((index + 1) % webviews.webviews().len()) + if let Some(index) = state.get_focused_webview_index() { + state.focus_webview_by_index((index + 1) % state.webviews().len()) } }) .shortcut(Modifiers::CONTROL, Key::PageUp, || { - if let Some(index) = webviews.get_focused_webview_index() { + if let Some(index) = state.get_focused_webview_index() { let new_index = if index == 0 { - webviews.webviews().len() - 1 + state.webviews().len() - 1 } else { index - 1 }; - webviews.focus_webview_by_index(new_index) + state.focus_webview_by_index(new_index) } }) .shortcut(CMD_OR_CONTROL, 'T', || { - webviews.add(servo.new_webview(Url::parse("servo:newtab").unwrap())); + state.new_toplevel_webview(Url::parse("servo:newtab").unwrap()); }) - .shortcut(CMD_OR_CONTROL, 'Q', || servo.start_shutting_down()) + .shortcut(CMD_OR_CONTROL, 'Q', || state.servo().start_shutting_down()) .otherwise(|| handled = false); handled } @@ -442,7 +429,7 @@ impl WindowPortsMethods for Window { self.winit_window.set_title(title); } - fn request_resize(&self, _: &ServoShellWebView, size: DeviceIntSize) -> Option { + fn request_resize(&self, _: &WebView, size: DeviceIntSize) -> Option { let toolbar_height = self.toolbar_height() * self.hidpi_factor(); let toolbar_height = toolbar_height.get().ceil() as i32; let total_size = PhysicalSize::new(size.width, size.height + toolbar_height); @@ -536,23 +523,14 @@ impl WindowPortsMethods for Window { self.winit_window.id() } - fn handle_winit_event( - &self, - servo: &Servo, - clipboard: &mut Option, - webviews: &mut WebViewManager, - event: winit::event::WindowEvent, - ) { - let Some(webview) = webviews - .focused_webview() - .map(|webview| webview.servo_webview.clone()) - else { + fn handle_winit_event(&self, state: Rc, event: winit::event::WindowEvent) { + let Some(webview) = state.focused_webview() else { return; }; match event { winit::event::WindowEvent::KeyboardInput { event, .. } => { - self.handle_keyboard_input(servo, clipboard, webviews, event) + self.handle_keyboard_input(state, event) }, winit::event::WindowEvent::ModifiersChanged(modifiers) => { self.modifiers_state.set(modifiers.state()) @@ -615,7 +593,7 @@ impl WindowPortsMethods for Window { webview.set_pinch_zoom(delta as f32 + 1.0); }, winit::event::WindowEvent::CloseRequested => { - servo.start_shutting_down(); + state.servo().start_shutting_down(); }, winit::event::WindowEvent::Resized(new_size) => { if self.inner_size.get() != new_size { diff --git a/ports/servoshell/desktop/headless_window.rs b/ports/servoshell/desktop/headless_window.rs index f6a511a2a84..4fbb0a97deb 100644 --- a/ports/servoshell/desktop/headless_window.rs +++ b/ports/servoshell/desktop/headless_window.rs @@ -7,15 +7,13 @@ use std::cell::Cell; use std::rc::Rc; -use arboard::Clipboard; use euclid::num::Zero; use euclid::{Box2D, Length, Point2D, Scale, Size2D}; use servo::compositing::windowing::{AnimationState, EmbedderCoordinates, WindowMethods}; use servo::servo_geometry::DeviceIndependentPixel; use servo::webrender_api::units::{DeviceIntSize, DevicePixel}; -use servo::Servo; -use super::webview::{WebView, WebViewManager}; +use super::app_state::RunningAppState; use crate::desktop::window_trait::WindowPortsMethods; pub struct Window { @@ -58,10 +56,6 @@ impl Window { Rc::new(window) } - - pub fn new_uninit() -> Rc { - Self::new(Default::default(), None, None) - } } impl WindowPortsMethods for Window { @@ -69,7 +63,11 @@ impl WindowPortsMethods for Window { winit::window::WindowId::dummy() } - fn request_resize(&self, webview: &WebView, size: DeviceIntSize) -> Option { + fn request_resize( + &self, + webview: &::servo::WebView, + size: DeviceIntSize, + ) -> Option { // Surfman doesn't support zero-sized surfaces. let new_size = DeviceIntSize::new(size.width.max(1), size.height.max(1)); if self.inner_size.get() == new_size { @@ -81,7 +79,7 @@ impl WindowPortsMethods for Window { // Because we are managing the rendering surface ourselves, there will be no other // notification (such as from the display manager) that it has changed size, so we // must notify the compositor here. - webview.servo_webview.notify_rendering_context_resized(); + webview.notify_rendering_context_resized(); Some(new_size) } @@ -114,13 +112,7 @@ impl WindowPortsMethods for Window { self.animation_state.get() == AnimationState::Animating } - fn handle_winit_event( - &self, - _: &Servo, - _: &mut Option, - _: &mut WebViewManager, - _: winit::event::WindowEvent, - ) { + fn handle_winit_event(&self, _: Rc, _: winit::event::WindowEvent) { // Not expecting any winit events. } diff --git a/ports/servoshell/desktop/minibrowser.rs b/ports/servoshell/desktop/minibrowser.rs index 7bad583ae53..d7687cd431a 100644 --- a/ports/servoshell/desktop/minibrowser.rs +++ b/ports/servoshell/desktop/minibrowser.rs @@ -24,14 +24,14 @@ use servo::servo_geometry::DeviceIndependentPixel; use servo::servo_url::ServoUrl; use servo::webrender_api::units::DevicePixel; use servo::webrender_traits::SurfmanRenderingContext; -use servo::{LoadStatus, Servo}; +use servo::{LoadStatus, WebView}; use winit::event::{ElementState, MouseButton, WindowEvent}; use winit::event_loop::ActiveEventLoop; use winit::window::Window; +use super::app_state::RunningAppState; use super::egui_glue::EguiGlow; use super::geometry::winit_position_to_euclid_point; -use super::webview::{WebView, WebViewManager}; pub struct Minibrowser { pub context: EguiGlow, @@ -73,6 +73,12 @@ fn truncate_with_ellipsis(input: &str, max_length: usize) -> String { } } +impl Drop for Minibrowser { + fn drop(&mut self) { + self.context.destroy(); + } +} + impl Minibrowser { pub fn new( rendering_context: &SurfmanRenderingContext, @@ -173,12 +179,13 @@ impl Minibrowser { /// Draws a browser tab, checking for clicks and queues appropriate `MinibrowserEvent`s. /// Using a custom widget here would've been nice, but it doesn't seem as though egui /// supports that, so we arrange multiple Widgets in a way that they look connected. - fn browser_tab( - ui: &mut egui::Ui, - label: &str, - webview: &WebView, - event_queue: &mut Vec, - ) { + fn browser_tab(ui: &mut egui::Ui, webview: WebView, event_queue: &mut Vec) { + let label = match (webview.page_title(), webview.url()) { + (Some(title), _) if !title.is_empty() => title, + (_, Some(url)) => url.to_string(), + _ => "New Tab".into(), + }; + let old_item_spacing = ui.spacing().item_spacing; let old_visuals = ui.visuals().clone(); let active_bg_color = old_visuals.widgets.active.weak_bg_fill; @@ -214,10 +221,10 @@ impl Minibrowser { visuals.widgets.hovered.rounding = rounding; visuals.widgets.inactive.rounding = rounding; - let selected = webview.focused; + let selected = webview.focused(); let tab = ui.add(SelectableLabel::new( selected, - truncate_with_ellipsis(label, 20), + truncate_with_ellipsis(&label, 20), )); let tab = tab.on_hover_ui(|ui| { ui.label(label); @@ -244,22 +251,16 @@ impl Minibrowser { let close_button = ui.add(egui::Button::new("X").fill(fill_color)); *ui.visuals_mut() = old_visuals; if close_button.clicked() || close_button.middle_clicked() || tab.middle_clicked() { - event_queue.push(MinibrowserEvent::CloseWebView(webview.servo_webview.id())) + event_queue.push(MinibrowserEvent::CloseWebView(webview.id())) } else if !selected && tab.clicked() { - webview.servo_webview.focus(); + webview.focus(); } } /// Update the minibrowser, but don’t paint. /// If `servo_framebuffer_id` is given, set up a paint callback to blit its contents to our /// CentralPanel when [`Minibrowser::paint`] is called. - pub fn update( - &mut self, - window: &Window, - webviews: &mut WebViewManager, - servo: Option<&Servo>, - reason: &'static str, - ) { + pub fn update(&mut self, window: &Window, state: &RunningAppState, reason: &'static str) { let now = Instant::now(); trace!( "{:?} since last update ({})", @@ -277,9 +278,7 @@ impl Minibrowser { .. } = self; let widget_fbo = *widget_surface_fbo; - let servo_framebuffer_id = servo - .as_ref() - .and_then(|servo| servo.offscreen_framebuffer_id()); + let servo_framebuffer_id = state.servo().offscreen_framebuffer_id(); let _duration = context.run(window, |ctx| { // TODO: While in fullscreen add some way to mitigate the increased phishing risk // when not displaying the URL bar: https://github.com/servo/servo/issues/32443 @@ -362,13 +361,8 @@ impl Minibrowser { ui.available_size(), egui::Layout::left_to_right(egui::Align::Center), |ui| { - for (_, webview) in webviews.webviews().into_iter() { - let label = match (&webview.title, &webview.url) { - (Some(title), _) if !title.is_empty() => title, - (_, Some(url)) => &url.to_string(), - _ => "New Tab", - }; - Self::browser_tab(ui, label, webview, &mut event_queue.borrow_mut()); + for (_, webview) in state.webviews().into_iter() { + Self::browser_tab(ui, webview, &mut event_queue.borrow_mut()); } if ui.add(Minibrowser::toolbar_button("+")).clicked() { event_queue.borrow_mut().push(MinibrowserEvent::NewWebView); @@ -384,17 +378,14 @@ impl Minibrowser { let scale = Scale::<_, DeviceIndependentPixel, DevicePixel>::new(ctx.pixels_per_point()); - let Some(focused_webview_id) = webviews.focused_webview_id() else { - return; - }; - let Some(webview) = webviews.get_mut(focused_webview_id) else { - return; - }; egui::CentralPanel::default().show(ctx, |_| { - webview.update(ctx); + state.for_each_active_dialog(|dialog| dialog.update(ctx)); }); + let Some(webview) = state.focused_webview() else { + return; + }; CentralPanel::default() .frame(Frame::none()) .show(ctx, |ui| { @@ -407,9 +398,8 @@ impl Minibrowser { Point2D::new(x, y), Size2D::new(width, height), ) * scale; - if rect != webview.rect { - webview.rect = rect; - webview.servo_webview.move_resize(rect) + if rect != webview.rect() { + webview.move_resize(rect); } let min = ui.cursor().min; let size = ui.available_size(); @@ -489,13 +479,16 @@ impl Minibrowser { /// Updates the location field from the given [WebViewManager], unless the user has started /// editing it without clicking Go, returning true iff it has changed (needing an egui update). - pub fn update_location_in_toolbar(&mut self, browser: &mut WebViewManager) -> bool { + pub fn update_location_in_toolbar(&mut self, state: &RunningAppState) -> bool { // User edited without clicking Go? if self.location_dirty.get() { return false; } - match browser.current_url_string() { + let current_url_string = state + .focused_webview() + .and_then(|webview| Some(webview.url()?.to_string())); + match current_url_string { Some(location) if location != *self.location.get_mut() => { self.location = RefCell::new(location.to_owned()); true @@ -508,29 +501,32 @@ impl Minibrowser { self.location_dirty.set(dirty); } - /// Updates the spinner from the given [WebViewManager], returning true iff it has changed - /// (needing an egui update). - pub fn update_spinner_in_toolbar(&mut self, browser: &mut WebViewManager) -> bool { - let need_update = browser.load_status() != self.load_status; - self.load_status = browser.load_status(); - need_update + pub fn update_load_status(&mut self, state: &RunningAppState) -> bool { + let state_status = state + .focused_webview() + .map(|webview| webview.load_status()) + .unwrap_or(LoadStatus::Complete); + let old_status = std::mem::replace(&mut self.load_status, state_status); + old_status != self.load_status } - pub fn update_status_text(&mut self, browser: &mut WebViewManager) -> bool { - let need_update = browser.status_text() != self.status_text; - self.status_text = browser.status_text(); - need_update + pub fn update_status_text(&mut self, state: &RunningAppState) -> bool { + let state_status = state + .focused_webview() + .and_then(|webview| webview.status_text()); + let old_status = std::mem::replace(&mut self.status_text, state_status); + old_status != self.status_text } /// Updates all fields taken from the given [WebViewManager], such as the location field. /// Returns true iff the egui needs an update. - pub fn update_webview_data(&mut self, browser: &mut WebViewManager) -> bool { + pub fn update_webview_data(&mut self, state: &RunningAppState) -> bool { // Note: We must use the "bitwise OR" (|) operator here instead of "logical OR" (||) // because logical OR would short-circuit if any of the functions return true. // We want to ensure that all functions are called. The "bitwise OR" operator // does not short-circuit. - self.update_location_in_toolbar(browser) | - self.update_spinner_in_toolbar(browser) | - self.update_status_text(browser) + self.update_location_in_toolbar(state) | + self.update_load_status(state) | + self.update_status_text(state) } } diff --git a/ports/servoshell/desktop/mod.rs b/ports/servoshell/desktop/mod.rs index cf612e89272..c6c78f2f799 100644 --- a/ports/servoshell/desktop/mod.rs +++ b/ports/servoshell/desktop/mod.rs @@ -5,11 +5,13 @@ //! Contains files specific to the servoshell app for Desktop systems. pub(crate) mod app; +mod app_state; pub(crate) mod cli; mod dialog; mod egui_glue; mod embedder; pub(crate) mod events_loop; +mod gamepad; pub mod geometry; mod headed_window; mod headless_window; @@ -17,5 +19,4 @@ mod keyutils; mod minibrowser; mod protocols; mod tracing; -mod webview; mod window_trait; diff --git a/ports/servoshell/desktop/tracing.rs b/ports/servoshell/desktop/tracing.rs index eedee21ca65..dfdfdc1602a 100644 --- a/ports/servoshell/desktop/tracing.rs +++ b/ports/servoshell/desktop/tracing.rs @@ -22,21 +22,7 @@ macro_rules! trace_winit_event { }; } -/// Log an event from servo ([servo::EmbedderMsg]) at trace level. -/// - To disable tracing: RUST_LOG='servoshell { - ::log::trace!(target: $crate::desktop::tracing::LogTarget::log_target(&$event), $($rest)+) - }; -} - -pub(crate) use {trace_embedder_msg, trace_winit_event}; +pub(crate) use trace_winit_event; /// Get the log target for an event, as a static string. pub(crate) trait LogTarget { @@ -115,58 +101,3 @@ mod from_winit { } } } - -mod from_servo { - use super::LogTarget; - - macro_rules! target { - ($($name:literal)+) => { - concat!("servoshell &'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"), - } - } - } -} diff --git a/ports/servoshell/desktop/webview.rs b/ports/servoshell/desktop/webview.rs deleted file mode 100644 index f4d615a1ef2..00000000000 --- a/ports/servoshell/desktop/webview.rs +++ /dev/null @@ -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, - - /// List of top-level browsing contexts. - /// Modified by EmbedderMsg::WebViewOpened and EmbedderMsg::WebViewClosed, - /// and we exit if it ever becomes empty. - webviews: HashMap, - - /// The order in which the webviews were created. - creation_order: Vec, - - /// The webview that is currently focused. - /// Modified by EmbedderMsg::WebViewFocused and EmbedderMsg::WebViewBlurred. - focused_webview_id: Option, - - window: Rc, - gamepad: Option, - haptic_effects: HashMap, - shutdown_requested: bool, -} - -// The state of each Tab/WebView -pub struct WebView { - pub rect: DeviceRect, - pub title: Option, - pub url: Option, - pub focused: bool, - pub load_status: LoadStatus, - pub servo_webview: ::servo::WebView, - dialogs: Vec, -} - -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>>, - patterns: Vec, - ) { - 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, -} - -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) { - 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 { - 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 { - 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 { - 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 = 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 - // - 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 - // - 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, - ) { - 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 { - 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, - opts: &Opts, - messages: Vec, - ) -> 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) -> Option { - 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) -> Option { - 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() -} diff --git a/ports/servoshell/desktop/window_trait.rs b/ports/servoshell/desktop/window_trait.rs index 8f481140273..da3f2e4c82e 100644 --- a/ports/servoshell/desktop/window_trait.rs +++ b/ports/servoshell/desktop/window_trait.rs @@ -7,14 +7,13 @@ use std::rc::Rc; -use arboard::Clipboard; use euclid::{Length, Scale}; use servo::compositing::windowing::WindowMethods; use servo::servo_geometry::DeviceIndependentPixel; use servo::webrender_api::units::{DeviceIntPoint, DeviceIntSize, DevicePixel}; -use servo::{Cursor, Servo}; +use servo::{Cursor, WebView}; -use super::webview::{WebView, WebViewManager}; +use super::app_state::RunningAppState; // This should vary by zoom level and maybe actual text size (focused or under cursor) pub const LINE_HEIGHT: f32 = 38.0; @@ -31,13 +30,7 @@ pub trait WindowPortsMethods: WindowMethods { ) -> Option>; fn page_height(&self) -> f32; fn get_fullscreen(&self) -> bool; - fn handle_winit_event( - &self, - servo: &Servo, - clipboard: &mut Option, - webviews: &mut WebViewManager, - event: winit::event::WindowEvent, - ); + fn handle_winit_event(&self, state: Rc, event: winit::event::WindowEvent); fn is_animating(&self) -> bool; fn set_title(&self, _title: &str) {} /// Request a new inner size for the window, not including external decorations.