diff options
author | Delan Azabani <dazabani@igalia.com> | 2025-02-05 18:08:40 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-02-05 10:08:40 +0000 |
commit | 175f28866dc254a98c4a911eb38ed9b200fdc6d1 (patch) | |
tree | e5f89c66cd473e9ed76c66a057e4bc3cc5451fb0 | |
parent | 789736590b0dd0806bffeb279f9a9bda9ede0dfc (diff) | |
download | servo-175f28866dc254a98c4a911eb38ed9b200fdc6d1.tar.gz servo-175f28866dc254a98c4a911eb38ed9b200fdc6d1.zip |
libservo: Add WebViewDelegate and ServoDelegate and port `winit_minimal` (#35196)
This change adds the second major part of the new API: delegates which
have methods called by the Servo loop. When a delegate is set on a
`WebView` or on `Servo` itself, the event loop will call into
appropriate delegate methods. Applications can implement the delegate on
their own structs to add special behavior per-`WebView` or for all
`WebView`s.
In addition, each delegate has a default implementation, which
automatically exposes "reasonable" behavior such as by-default allowing
navigation.
There's a lot more work to do here, such as refining the delegate
methods so that they all have nice interfaces, particulary with regard
to delegate methods that need an asynchronous response. This will be
handed gradually as we keep working on the API.
Signed-off-by: Delan Azabani <dazabani@igalia.com>
Signed-off-by: Martin Robinson <mrobinson@igalia.com>
Co-authored-by: Martin Robinson <mrobinson@igalia.com>
Co-authored-by: Mukilan Thiyagarajan <mukilan@igalia.com>
-rw-r--r-- | components/servo/examples/winit_minimal.rs | 153 | ||||
-rw-r--r-- | components/servo/lib.rs | 421 | ||||
-rw-r--r-- | components/servo/proxies.rs | 6 | ||||
-rw-r--r-- | components/servo/servo_delegate.rs | 47 | ||||
-rw-r--r-- | components/servo/webview.rs | 277 | ||||
-rw-r--r-- | components/servo/webview_delegate.rs | 262 | ||||
-rw-r--r-- | components/shared/embedder/lib.rs | 4 | ||||
-rw-r--r-- | ports/servoshell/desktop/app.rs | 4 | ||||
-rw-r--r-- | ports/servoshell/desktop/webview.rs | 22 | ||||
-rw-r--r-- | ports/servoshell/egl/servo_glue.rs | 5 |
10 files changed, 1004 insertions, 197 deletions
diff --git a/components/servo/examples/winit_minimal.rs b/components/servo/examples/winit_minimal.rs index 8a7ea1a78d5..d7a2a43584f 100644 --- a/components/servo/examples/winit_minimal.rs +++ b/components/servo/examples/winit_minimal.rs @@ -2,19 +2,19 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use std::cell::Cell; +use std::cell::{Cell, RefCell}; use std::error::Error; use std::rc::Rc; -use compositing::windowing::{AnimationState, EmbedderEvent, EmbedderMethods, WindowMethods}; -use embedder_traits::EmbedderMsg; +use compositing::windowing::{AnimationState, EmbedderMethods, WindowMethods}; use euclid::{Point2D, Scale, Size2D}; -use servo::{Servo, WebView}; +use servo::{Servo, TouchEventType, WebView}; use servo_geometry::DeviceIndependentPixel; use surfman::{Connection, SurfaceType}; use tracing::warn; use url::Url; -use webrender_api::units::{DeviceIntPoint, DeviceIntRect, DevicePixel}; +use webrender_api::units::{DeviceIntPoint, DeviceIntRect, DevicePixel, LayoutVector2D}; +use webrender_api::ScrollLocation; use webrender_traits::SurfmanRenderingContext; use winit::application::ApplicationHandler; use winit::dpi::{PhysicalPosition, PhysicalSize}; @@ -34,20 +34,48 @@ fn main() -> Result<(), Box<dyn Error>> { let mut app = App::new(&event_loop); event_loop.run_app(&mut app)?; - if let App::Running { servo, .. } = app { - servo.deinit(); + if let App::Running(state) = app { + if let Some(state) = Rc::into_inner(state) { + state.servo.deinit(); + } } Ok(()) } +struct AppState { + window_delegate: Rc<WindowDelegate>, + servo: Servo, + webviews: RefCell<Vec<WebView>>, +} + +impl ::servo::WebViewDelegate for AppState { + fn notify_ready_to_show(&self, webview: WebView) { + let rect = self + .window_delegate + .get_coordinates() + .get_viewport() + .to_f32(); + webview.focus(); + webview.move_resize(rect); + webview.raise_to_top(true); + } + + fn notify_new_frame_ready(&self, _: WebView) { + self.servo.present(); + } + + fn request_open_auxiliary_webview(&self, parent_webview: WebView) -> Option<WebView> { + let webview = self.servo.new_auxiliary_webview(); + webview.set_delegate(parent_webview.delegate()); + self.webviews.borrow_mut().push(webview.clone()); + Some(webview) + } +} + enum App { Initial(Waker), - Running { - window_delegate: Rc<WindowDelegate>, - servo: Servo, - webviews: Vec<WebView>, - }, + Running(Rc<AppState>), } impl App { @@ -103,15 +131,20 @@ impl ApplicationHandler<WakerEvent> for App { compositing::CompositeTarget::ContextFbo, ); servo.setup_logging(); - let webviews = vec![servo.new_webview( - Url::parse("https://demo.servo.org/experiments/twgl-tunnel/") - .expect("Guaranteed by argument"), - )]; - *self = Self::Running { + + let app_state = Rc::new(AppState { window_delegate, servo, - webviews, - }; + webviews: Default::default(), + }); + + // Make a new WebView and assign the `AppState` as the delegate. + let url = Url::parse("https://servo.org").expect("Guaranteed by argument"); + let webview = app_state.servo.new_webview(url); + webview.set_delegate(app_state.clone()); + app_state.webviews.borrow_mut().push(webview); + + *self = Self::Running(app_state); } } @@ -121,69 +154,41 @@ impl ApplicationHandler<WakerEvent> for App { _window_id: winit::window::WindowId, event: WindowEvent, ) { - if let Self::Running { - window_delegate, - servo, - webviews, - } = self - { - for message in servo.get_events().collect::<Vec<_>>() { - match message { - // FIXME: rust-analyzer autocompletes this as top_level_browsing_context_id - EmbedderMsg::WebViewOpened(webview_id) => { - // TODO: We currently assume `webview` refers to the same webview as `_webview_id` - let rect = window_delegate.get_coordinates().get_viewport().to_f32(); - if let Some(webview) = - webviews.iter().find(|webview| webview.id() == webview_id) - { - webview.focus(); - webview.move_resize(rect); - webview.raise_to_top(true); - } - }, - EmbedderMsg::AllowOpeningWebView(_, webview_id_sender) => { - let webview = servo.new_auxiliary_webview(); - let _ = webview_id_sender.send(Some(webview.id())); - webviews.push(webview); - }, - EmbedderMsg::AllowNavigationRequest(_, pipeline_id, _) => { - servo.handle_events([EmbedderEvent::AllowNavigationResponse( - pipeline_id, - true, - )]); - }, - _ => {}, - } - } - // FIXME: still needed for the compositor to actually run - servo.handle_events([]); + if let Self::Running(state) = self { + state.servo.spin_event_loop(); } + match event { WindowEvent::CloseRequested => { event_loop.exit(); }, WindowEvent::RedrawRequested => { - if let Self::Running { - window_delegate, - servo, - .. - } = self - { - servo.present(); - window_delegate.window.request_redraw(); + if let Self::Running(state) = self { + state.webviews.borrow().last().unwrap().composite(); + state.servo.present(); } }, - WindowEvent::MouseInput { .. } => { - // When the window is clicked, close the last webview by dropping its handle, - // then show the next most recently opened webview. - // - // TODO: Test closing webviews a better way, so that we can use mouse input to test - // input handling. - if let Self::Running { webviews, .. } = self { - let _ = webviews.pop(); - match webviews.last() { - Some(last) => last.show(true), - None => event_loop.exit(), + WindowEvent::MouseWheel { .. } => { + if let Self::Running(state) = self { + if let Some(webview) = state.webviews.borrow().last() { + webview.notify_scroll_event( + ScrollLocation::Delta(LayoutVector2D::new(0., -20.)), + DeviceIntPoint::new(10, 10), + TouchEventType::Down, + ); + } + } + }, + WindowEvent::KeyboardInput { event, .. } => { + // When pressing 'q' close the latest WebView, then show the next most recently + // opened view or quit when none are left. + if event.logical_key.to_text() == Some("q") { + if let Self::Running(state) = self { + let _ = state.webviews.borrow_mut().pop(); + match state.webviews.borrow().last() { + Some(last) => last.show(true), + None => event_loop.exit(), + } } } }, diff --git a/components/servo/lib.rs b/components/servo/lib.rs index b21445fda84..7e961f35f20 100644 --- a/components/servo/lib.rs +++ b/components/servo/lib.rs @@ -18,19 +18,21 @@ //! `WindowMethods` trait. mod proxies; +mod servo_delegate; mod webview; +mod webview_delegate; use std::borrow::Cow; use std::cell::RefCell; use std::cmp::max; +use std::collections::HashMap; use std::path::PathBuf; -use std::rc::Rc; +use std::rc::{Rc, Weak}; use std::sync::{Arc, Mutex}; use std::thread; -use std::vec::Drain; pub use base::id::TopLevelBrowsingContextId; -use base::id::{PipelineId, PipelineNamespace, PipelineNamespaceId}; +use base::id::{PipelineId, PipelineNamespace, PipelineNamespaceId, WebViewId}; use bluetooth::BluetoothThreadFactory; use bluetooth_traits::BluetoothRequest; use canvas::canvas_paint_thread::CanvasPaintThread; @@ -86,8 +88,10 @@ use script_traits::{ScriptToConstellationChan, WindowSizeData}; use servo_config::opts::Opts; use servo_config::prefs::Preferences; use servo_config::{opts, pref, prefs}; +use servo_delegate::DefaultServoDelegate; use servo_media::player::context::GlContext; use servo_media::ServoMedia; +use servo_url::ServoUrl; #[cfg(all(target_os = "linux", not(target_env = "ohos")))] use surfman::platform::generic::multi::connection::NativeConnection as LinuxNativeConnection; #[cfg(all(target_os = "linux", not(target_env = "ohos")))] @@ -106,6 +110,7 @@ use webrender_traits::{ CrossProcessCompositorApi, WebrenderExternalImageHandlers, WebrenderExternalImageRegistry, WebrenderImageHandlerType, }; +use webview::WebViewInner; #[cfg(feature = "webxr")] pub use webxr; pub use { @@ -117,7 +122,9 @@ pub use { }; use crate::proxies::ConstellationProxy; +pub use crate::servo_delegate::{ServoDelegate, ServoError}; pub use crate::webview::WebView; +pub use crate::webview_delegate::{AllowOrDenyRequest, NavigationRequest, WebViewDelegate}; #[cfg(feature = "webdriver")] fn webdriver(port: u16, constellation: Sender<ConstellationMsg>) { @@ -187,10 +194,16 @@ mod media_platform { /// loop to pump messages between the embedding application and /// various browser components. pub struct Servo { + delegate: Rc<dyn ServoDelegate>, compositor: Rc<RefCell<IOCompositor>>, constellation_proxy: ConstellationProxy, embedder_receiver: Receiver<EmbedderMsg>, - messages_for_embedder: Vec<EmbedderMsg>, + messages_for_embedder: RefCell<Vec<EmbedderMsg>>, + /// A map [`WebView`]s that are managed by this [`Servo`] instance. These are stored + /// as `Weak` references so that the embedding application can control their lifetime. + /// When accessed, `Servo` will be reponsible for cleaning up the invalid `Weak` + /// references. + webviews: RefCell<HashMap<WebViewId, Weak<RefCell<WebViewInner>>>>, /// For single-process Servo instances, this field controls the initialization /// and deinitialization of the JS Engine. Multiprocess Servo instances have their /// own instance that exists in the content process instead. @@ -527,14 +540,24 @@ impl Servo { ); Servo { + delegate: Rc::new(DefaultServoDelegate), compositor: Rc::new(RefCell::new(compositor)), constellation_proxy: ConstellationProxy::new(constellation_chan), embedder_receiver, - messages_for_embedder: Vec::new(), + messages_for_embedder: Default::default(), + webviews: Default::default(), _js_engine_setup: js_engine_setup, } } + pub fn delegate(&self) -> Rc<dyn ServoDelegate> { + self.delegate.clone() + } + + pub fn set_delegate(&mut self, delegate: Rc<dyn ServoDelegate>) { + self.delegate = delegate; + } + #[cfg(all(target_os = "linux", not(target_env = "ohos")))] fn get_native_media_display_and_gl_context( rendering_context: &Rc<dyn RenderingContext>, @@ -678,8 +701,8 @@ impl Servo { } }, - EmbedderEvent::LoadUrl(top_level_browsing_context_id, url) => { - let msg = ConstellationMsg::LoadUrl(top_level_browsing_context_id, url); + EmbedderEvent::LoadUrl(webview_id, url) => { + let msg = ConstellationMsg::LoadUrl(webview_id, url); if let Err(e) = self.constellation_proxy.try_send(msg) { warn!("Sending load url to constellation failed ({:?}).", e); } @@ -742,6 +765,7 @@ impl Servo { warn!("Sending navigation to constellation failed ({:?}).", e); } self.messages_for_embedder + .borrow_mut() .push(EmbedderMsg::Status(webview_id, None)); }, @@ -776,15 +800,15 @@ impl Servo { self.compositor.borrow_mut().maybe_start_shutting_down(); }, - EmbedderEvent::ExitFullScreen(top_level_browsing_context_id) => { - let msg = ConstellationMsg::ExitFullScreen(top_level_browsing_context_id); + EmbedderEvent::ExitFullScreen(webview_id) => { + let msg = ConstellationMsg::ExitFullScreen(webview_id); if let Err(e) = self.constellation_proxy.try_send(msg) { warn!("Sending exit fullscreen to constellation failed ({:?}).", e); } }, - EmbedderEvent::Reload(top_level_browsing_context_id) => { - let msg = ConstellationMsg::Reload(top_level_browsing_context_id); + EmbedderEvent::Reload(webview_id) => { + let msg = ConstellationMsg::Reload(webview_id); if let Err(e) = self.constellation_proxy.try_send(msg) { warn!("Sending reload to constellation failed ({:?}).", e); } @@ -807,8 +831,8 @@ impl Servo { self.compositor.borrow_mut().capture_webrender(); }, - EmbedderEvent::NewWebView(url, top_level_browsing_context_id) => { - let msg = ConstellationMsg::NewWebView(url, top_level_browsing_context_id); + EmbedderEvent::NewWebView(url, webview_id) => { + let msg = ConstellationMsg::NewWebView(url, webview_id); if let Err(e) = self.constellation_proxy.try_send(msg) { warn!( "Sending NewBrowser message to constellation failed ({:?}).", @@ -817,8 +841,8 @@ impl Servo { } }, - EmbedderEvent::FocusWebView(top_level_browsing_context_id) => { - let msg = ConstellationMsg::FocusWebView(top_level_browsing_context_id); + EmbedderEvent::FocusWebView(webview_id) => { + let msg = ConstellationMsg::FocusWebView(webview_id); if let Err(e) = self.constellation_proxy.try_send(msg) { warn!( "Sending FocusBrowser message to constellation failed ({:?}).", @@ -827,8 +851,8 @@ impl Servo { } }, - EmbedderEvent::CloseWebView(top_level_browsing_context_id) => { - let msg = ConstellationMsg::CloseWebView(top_level_browsing_context_id); + EmbedderEvent::CloseWebView(webview_id) => { + let msg = ConstellationMsg::CloseWebView(webview_id); if let Err(e) = self.constellation_proxy.try_send(msg) { warn!( "Sending CloseBrowser message to constellation failed ({:?}).", @@ -871,8 +895,8 @@ impl Servo { self.send_to_constellation(ConstellationMsg::BlurWebView); }, - EmbedderEvent::SendError(top_level_browsing_context_id, e) => { - let msg = ConstellationMsg::SendError(top_level_browsing_context_id, e); + EmbedderEvent::SendError(webview_id, e) => { + let msg = ConstellationMsg::SendError(webview_id, e); if let Err(e) = self.constellation_proxy.try_send(msg) { warn!( "Sending SendError message to constellation failed ({:?}).", @@ -923,7 +947,7 @@ impl Servo { } } - fn receive_messages(&mut self) { + fn receive_messages(&self) { while let Ok(message) = self.embedder_receiver.try_recv() { match (message, self.compositor.borrow().shutdown_state) { (_, ShutdownState::FinishedShuttingDown) => { @@ -936,18 +960,19 @@ impl Servo { (EmbedderMsg::Keyboard(webview_id, key_event), ShutdownState::NotShuttingDown) => { self.messages_for_embedder + .borrow_mut() .push(EmbedderMsg::Keyboard(webview_id, key_event)); }, (message, ShutdownState::NotShuttingDown) => { - self.messages_for_embedder.push(message); + self.messages_for_embedder.borrow_mut().push(message); }, } } } - pub fn get_events(&mut self) -> Drain<'_, EmbedderMsg> { - self.messages_for_embedder.drain(..) + pub fn get_events(&self) -> Vec<EmbedderMsg> { + std::mem::take(&mut *self.messages_for_embedder.borrow_mut()) } pub fn handle_events(&mut self, events: impl IntoIterator<Item = EmbedderEvent>) { @@ -961,8 +986,38 @@ impl Servo { if self.compositor.borrow().shutdown_state != ShutdownState::FinishedShuttingDown { self.compositor.borrow_mut().perform_updates(); } else { - self.messages_for_embedder.push(EmbedderMsg::Shutdown); + self.messages_for_embedder + .borrow_mut() + .push(EmbedderMsg::Shutdown); + } + } + + /// Spin the Servo event loop, which: + /// + /// - Performs updates in the compositor, such as queued pinch zoom events + /// - Runs delebgate methods on all `WebView`s and `Servo` itself + /// - Maybe update the rendered compositor output, but *without* swapping buffers. + /// + /// The return value of this method indicates whether or not Servo, false indicates that Servo + /// has finished shutting down and you should not spin the event loop any longer. + pub fn spin_event_loop(&self) -> bool { + if self.compositor.borrow_mut().receive_messages() { + self.receive_messages(); + } + + self.call_delegate_methods(); + if self.constellation_proxy.disconnected() { + self.delegate() + .notify_error(self, ServoError::LostConnectionWithBackend); } + + self.compositor.borrow_mut().perform_updates(); + + if self.compositor.borrow().shutdown_state == ShutdownState::FinishedShuttingDown { + return false; + } + + true } pub fn pinch_zoom_level(&self) -> f32 { @@ -996,11 +1051,11 @@ impl Servo { } } - pub fn deinit(self) { + pub fn deinit(&self) { self.compositor.borrow_mut().deinit(); } - pub fn present(&mut self) { + pub fn present(&self) { self.compositor.borrow_mut().present(); } @@ -1011,12 +1066,320 @@ impl Servo { } pub fn new_webview(&self, url: url::Url) -> WebView { - WebView::new(&self.constellation_proxy, self.compositor.clone(), url) + let webview = WebView::new(&self.constellation_proxy, self.compositor.clone()); + self.webviews + .borrow_mut() + .insert(webview.id(), webview.weak_handle()); + self.constellation_proxy + .send(ConstellationMsg::NewWebView(url.into(), webview.id())); + webview } - /// FIXME: Remove this once we have a webview delegate. pub fn new_auxiliary_webview(&self) -> WebView { - WebView::new_auxiliary(&self.constellation_proxy, self.compositor.clone()) + let webview = WebView::new(&self.constellation_proxy, self.compositor.clone()); + self.webviews + .borrow_mut() + .insert(webview.id(), webview.weak_handle()); + webview + } + + fn get_webview_handle(&self, id: WebViewId) -> Option<WebView> { + self.webviews + .borrow() + .get(&id) + .and_then(WebView::from_weak_handle) + } + + fn call_delegate_methods(&self) { + let events = self.get_events(); + for event in events { + match event { + EmbedderMsg::Status(webview_id, status_text) => { + if let Some(webview) = self.get_webview_handle(webview_id) { + webview.set_status_text(status_text); + } + }, + EmbedderMsg::ChangePageTitle(webview_id, title) => { + if let Some(webview) = self.get_webview_handle(webview_id) { + webview.set_page_title(title); + } + }, + EmbedderMsg::MoveTo(webview_id, position) => { + if let Some(webview) = self.get_webview_handle(webview_id) { + webview.delegate().request_move_to(webview, position); + } + }, + EmbedderMsg::ResizeTo(webview_id, size) => { + if let Some(webview) = self.get_webview_handle(webview_id) { + webview.delegate().request_resize_to(webview, size); + } + }, + EmbedderMsg::Prompt(webview_id, prompt_definition, prompt_origin) => { + if let Some(webview) = self.get_webview_handle(webview_id) { + webview + .delegate() + .show_prompt(webview, prompt_definition, prompt_origin); + } + }, + EmbedderMsg::ShowContextMenu(webview_id, ipc_sender, title, items) => { + if let Some(webview) = self.get_webview_handle(webview_id) { + webview + .delegate() + .show_context_menu(webview, ipc_sender, title, items); + } + }, + EmbedderMsg::AllowNavigationRequest(webview_id, pipeline_id, servo_url) => { + if let Some(webview) = self.get_webview_handle(webview_id) { + let request = NavigationRequest { + url: servo_url.into_url(), + pipeline_id, + constellation_proxy: self.constellation_proxy.clone(), + response_sent: false, + }; + webview.delegate().request_navigation(webview, request); + } + }, + EmbedderMsg::AllowOpeningWebView(webview_id, response_sender) => { + if let Some(webview) = self.get_webview_handle(webview_id) { + let new_webview = + webview.delegate().request_open_auxiliary_webview(webview); + let _ = response_sender.send(new_webview.map(|webview| webview.id())); + } + }, + EmbedderMsg::WebViewOpened(webview_id) => { + if let Some(webview) = self.get_webview_handle(webview_id) { + webview.delegate().notify_ready_to_show(webview); + } + }, + EmbedderMsg::WebViewClosed(_) => {}, + EmbedderMsg::WebViewFocused(webview_id) => { + for id in self.webviews.borrow().keys() { + if let Some(webview) = self.get_webview_handle(*id) { + let focused = webview.id() == webview_id; + webview.set_focused(focused); + } + } + }, + EmbedderMsg::WebViewBlurred => { + for id in self.webviews.borrow().keys() { + if let Some(webview) = self.get_webview_handle(*id) { + webview.set_focused(false); + } + } + }, + EmbedderMsg::AllowUnload(webview_id, response_sender) => { + if let Some(webview) = self.get_webview_handle(webview_id) { + let request = AllowOrDenyRequest { + response_sender, + response_sent: false, + default_response: true, + }; + webview.delegate().request_unload(webview, request); + } + }, + EmbedderMsg::Keyboard(webview_id, keyboard_event) => { + if let Some(webview) = self.get_webview_handle(webview_id) { + webview + .delegate() + .notify_keyboard_event(webview, keyboard_event); + } + }, + EmbedderMsg::ClearClipboardContents(webview_id) => { + if let Some(webview) = self.get_webview_handle(webview_id) { + webview.delegate().clear_clipboard_contents(webview); + } + }, + EmbedderMsg::GetClipboardContents(webview_id, ipc_sender) => { + if let Some(webview) = self.get_webview_handle(webview_id) { + webview + .delegate() + .get_clipboard_contents(webview, ipc_sender); + } + }, + EmbedderMsg::SetClipboardContents(webview_id, string) => { + if let Some(webview) = self.get_webview_handle(webview_id) { + webview.delegate().set_clipboard_contents(webview, string); + } + }, + EmbedderMsg::SetCursor(webview_id, cursor) => { + if let Some(webview) = self.get_webview_handle(webview_id) { + webview.set_cursor(cursor); + } + }, + EmbedderMsg::NewFavicon(webview_id, url) => { + if let Some(webview) = self.get_webview_handle(webview_id) { + webview.set_favicon_url(url.into_url()); + } + }, + EmbedderMsg::NotifyLoadStatusChanged(webview_id, load_status) => { + if let Some(webview) = self.get_webview_handle(webview_id) { + webview.set_load_status(load_status); + } + }, + EmbedderMsg::HistoryChanged(webview_id, urls, current_index) => { + if let Some(webview) = self.get_webview_handle(webview_id) { + let urls: Vec<_> = urls.into_iter().map(ServoUrl::into_url).collect(); + let current_url = urls[current_index].clone(); + + webview.delegate().notify_history_changed( + webview.clone(), + urls, + current_index, + ); + webview.set_url(current_url); + } + }, + EmbedderMsg::SetFullscreenState(webview_id, fullscreen) => { + if let Some(webview) = self.get_webview_handle(webview_id) { + webview + .delegate() + .request_fullscreen_state_change(webview, fullscreen); + } + }, + EmbedderMsg::WebResourceRequested( + webview_id, + web_resource_request, + response_sender, + ) => { + let webview = + webview_id.and_then(|webview_id| self.get_webview_handle(webview_id)); + if let Some(webview) = webview.clone() { + webview.delegate().intercept_web_resource_load( + webview, + &web_resource_request, + response_sender.clone(), + ); + } + + self.delegate().intercept_web_resource_load( + webview, + &web_resource_request, + response_sender, + ); + }, + EmbedderMsg::Panic(webview_id, reason, backtrace) => { + if let Some(webview) = self.get_webview_handle(webview_id) { + webview + .delegate() + .notify_crashed(webview, reason, backtrace); + } + }, + EmbedderMsg::GetSelectedBluetoothDevice(webview_id, items, response_sender) => { + if let Some(webview) = self.get_webview_handle(webview_id) { + webview.delegate().show_bluetooth_device_dialog( + webview, + items, + response_sender, + ); + } + }, + EmbedderMsg::SelectFiles( + webview_id, + filter_patterns, + allow_select_multiple, + response_sender, + ) => { + if let Some(webview) = self.get_webview_handle(webview_id) { + webview.delegate().show_file_selection_dialog( + webview, + filter_patterns, + allow_select_multiple, + response_sender, + ); + } + }, + EmbedderMsg::PromptPermission(webview_id, permission_prompt, result_sender) => { + if let Some(webview) = self.get_webview_handle(webview_id) { + webview.delegate().request_permission( + webview, + permission_prompt, + result_sender, + ); + } + }, + EmbedderMsg::ShowIME(webview_id, input_method_type, text, multiline, position) => { + if let Some(webview) = self.get_webview_handle(webview_id) { + webview.delegate().show_ime( + webview, + input_method_type, + text, + multiline, + position, + ); + } + }, + EmbedderMsg::HideIME(webview_id) => { + if let Some(webview) = self.get_webview_handle(webview_id) { + webview.delegate().hide_ime(webview); + } + }, + EmbedderMsg::ReportProfile(_items) => {}, + EmbedderMsg::MediaSessionEvent(webview_id, media_session_event) => { + if let Some(webview) = self.get_webview_handle(webview_id) { + webview + .delegate() + .notify_media_session_event(webview, media_session_event); + } + }, + EmbedderMsg::OnDevtoolsStarted(port, token) => match port { + Ok(port) => self + .delegate() + .notify_devtools_server_started(self, port, token), + Err(()) => self + .delegate() + .notify_error(self, ServoError::DevtoolsFailedToStart), + }, + EmbedderMsg::RequestDevtoolsConnection(response_sender) => { + self.delegate().request_devtools_connection( + self, + AllowOrDenyRequest { + response_sender, + response_sent: false, + default_response: false, + }, + ); + }, + EmbedderMsg::ReadyToPresent(webview_ids) => { + for webview_id in webview_ids { + if let Some(webview) = self.get_webview_handle(webview_id) { + webview.delegate().notify_new_frame_ready(webview); + } + } + }, + EmbedderMsg::EventDelivered(webview_id, event) => { + if let Some(webview) = self.get_webview_handle(webview_id) { + webview.delegate().notify_event_delivered(webview, event); + } + }, + EmbedderMsg::PlayGamepadHapticEffect( + webview_id, + gamepad_index, + gamepad_haptic_effect_type, + ipc_sender, + ) => { + if let Some(webview) = self.get_webview_handle(webview_id) { + webview.delegate().play_gamepad_haptic_effect( + webview, + gamepad_index, + gamepad_haptic_effect_type, + ipc_sender, + ); + } + }, + EmbedderMsg::StopGamepadHapticEffect(webview_id, gamepad_index, ipc_sender) => { + if let Some(webview) = self.get_webview_handle(webview_id) { + webview.delegate().stop_gamepad_haptic_effect( + webview, + gamepad_index, + ipc_sender, + ); + } + }, + EmbedderMsg::Shutdown => { + // This message isn't necessary in the new API -- it's the return value of `spin_event_loop`. + }, + } + } } } diff --git a/components/servo/proxies.rs b/components/servo/proxies.rs index 526eeea21f3..248e1e66f66 100644 --- a/components/servo/proxies.rs +++ b/components/servo/proxies.rs @@ -23,6 +23,10 @@ impl ConstellationProxy { } } + pub fn disconnected(&self) -> bool { + self.disconnected.load(Ordering::SeqCst) + } + pub fn send(&self, msg: ConstellationMsg) { if self.try_send(msg).is_err() { warn!("Lost connection to Constellation. Will report to embedder.") @@ -30,7 +34,7 @@ impl ConstellationProxy { } pub fn try_send(&self, msg: ConstellationMsg) -> Result<(), SendError<ConstellationMsg>> { - if self.disconnected.load(Ordering::SeqCst) { + if self.disconnected() { return Err(SendError(msg)); } if let Err(error) = self.sender.send(msg) { diff --git a/components/servo/servo_delegate.rs b/components/servo/servo_delegate.rs new file mode 100644 index 00000000000..379be8a8513 --- /dev/null +++ b/components/servo/servo_delegate.rs @@ -0,0 +1,47 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use embedder_traits::{WebResourceRequest, WebResourceResponseMsg}; +use ipc_channel::ipc::IpcSender; + +use crate::webview_delegate::AllowOrDenyRequest; +use crate::{Servo, WebView}; + +#[derive(Clone, Copy, Debug, Hash, PartialEq)] +pub enum ServoError { + /// The channel to the off-the-main-thread web engine has been lost. No further + /// attempts to communicate will happen. This is an unrecoverable error in Servo. + LostConnectionWithBackend, + /// The devtools server, used to expose pages to remote web inspectors has failed + /// to start. + DevtoolsFailedToStart, +} + +pub trait ServoDelegate { + /// Notification that Servo has received a major error. + fn notify_error(&self, _servo: &Servo, _error: ServoError) {} + /// Report that the DevTools server has started on the given `port`. The `token` that + /// be used to bypass the permission prompt from the DevTools client. + fn notify_devtools_server_started(&self, _servo: &Servo, _port: u16, _token: String) {} + /// Request a DevTools connection from a DevTools client. Typically an embedder application + /// will show a permissions prompt when this happens to confirm a connection is allowed. + fn request_devtools_connection(&self, _servo: &Servo, _request: AllowOrDenyRequest) {} + /// Potentially intercept a resource request. If not handled, the request will not be intercepted. + /// + /// Note: If this request is associated with a `WebView`, the `WebViewDelegate` will + /// receive this notification first and have a chance to intercept the request. + /// + /// TODO: This API needs to be reworked to match the new model of how responses are sent. + fn intercept_web_resource_load( + &self, + _webview: Option<WebView>, + _request: &WebResourceRequest, + response_sender: IpcSender<WebResourceResponseMsg>, + ) { + let _ = response_sender.send(WebResourceResponseMsg::None); + } +} + +pub(crate) struct DefaultServoDelegate; +impl ServoDelegate for DefaultServoDelegate {} diff --git a/components/servo/webview.rs b/components/servo/webview.rs index a47ccaf04ff..b2a286fad7a 100644 --- a/components/servo/webview.rs +++ b/components/servo/webview.rs @@ -2,9 +2,10 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use std::cell::RefCell; +use std::cell::{Ref, RefCell, RefMut}; use std::ffi::c_void; -use std::rc::Rc; +use std::hash::Hash; +use std::rc::{Rc, Weak}; use std::time::Duration; use base::id::WebViewId; @@ -12,24 +13,47 @@ use compositing::windowing::{MouseWindowEvent, WebRenderDebugOption}; use compositing::IOCompositor; use compositing_traits::ConstellationMsg; use embedder_traits::{ - ClipboardEventType, GamepadEvent, MediaSessionActionType, Theme, TouchEventType, TouchId, - TraversalDirection, WheelDelta, + ClipboardEventType, Cursor, GamepadEvent, LoadStatus, MediaSessionActionType, Theme, + TouchEventType, TouchId, TraversalDirection, WheelDelta, }; use keyboard_types::{CompositionEvent, KeyboardEvent}; use url::Url; use webrender_api::units::{DeviceIntPoint, DeviceIntSize, DevicePoint, DeviceRect}; use webrender_api::ScrollLocation; +use crate::webview_delegate::{DefaultWebViewDelegate, WebViewDelegate}; use crate::ConstellationProxy; #[derive(Clone)] -pub struct WebView(Rc<WebViewInner>); +pub struct WebView(Rc<RefCell<WebViewInner>>); -struct WebViewInner { +impl PartialEq for WebView { + fn eq(&self, other: &Self) -> bool { + self.inner().id == other.inner().id + } +} + +impl Hash for WebView { + fn hash<H: std::hash::Hasher>(&self, state: &mut H) { + self.inner().id.hash(state); + } +} + +pub(crate) struct WebViewInner { // TODO: ensure that WebView instances interact with the correct Servo instance pub(crate) id: WebViewId, pub(crate) constellation_proxy: ConstellationProxy, pub(crate) compositor: Rc<RefCell<IOCompositor>>, + pub(crate) delegate: Rc<dyn WebViewDelegate>, + + rect: DeviceRect, + load_status: LoadStatus, + url: Option<Url>, + status_text: Option<String>, + page_title: Option<String>, + favicon_url: Option<Url>, + focused: bool, + cursor: Cursor, } impl Drop for WebViewInner { @@ -49,61 +73,175 @@ impl WebView { pub(crate) fn new( constellation_proxy: &ConstellationProxy, compositor: Rc<RefCell<IOCompositor>>, - url: Url, ) -> Self { - let webview_id = WebViewId::new(); - constellation_proxy.send(ConstellationMsg::NewWebView(url.into(), webview_id)); - - Self(Rc::new(WebViewInner { - id: webview_id, + Self(Rc::new(RefCell::new(WebViewInner { + id: WebViewId::new(), constellation_proxy: constellation_proxy.clone(), compositor, - })) + delegate: Rc::new(DefaultWebViewDelegate), + rect: DeviceRect::zero(), + load_status: LoadStatus::Complete, + url: None, + status_text: None, + page_title: None, + favicon_url: None, + focused: false, + cursor: Cursor::Pointer, + }))) } - /// FIXME: Remove this once we have a webview delegate. - pub(crate) fn new_auxiliary( - constellation_proxy: &ConstellationProxy, - compositor: Rc<RefCell<IOCompositor>>, - ) -> Self { - let webview_id = WebViewId::new(); + fn inner(&self) -> Ref<'_, WebViewInner> { + self.0.borrow() + } + + fn inner_mut(&self) -> RefMut<'_, WebViewInner> { + self.0.borrow_mut() + } + + pub(crate) fn from_weak_handle(inner: &Weak<RefCell<WebViewInner>>) -> Option<Self> { + inner.upgrade().map(WebView) + } + + pub(crate) fn weak_handle(&self) -> Weak<RefCell<WebViewInner>> { + Rc::downgrade(&self.0) + } - Self( - WebViewInner { - id: webview_id, - constellation_proxy: constellation_proxy.clone(), - compositor, - } - .into(), - ) + pub fn delegate(&self) -> Rc<dyn WebViewDelegate> { + self.inner().delegate.clone() + } + + pub fn set_delegate(&self, delegate: Rc<dyn WebViewDelegate>) { + self.inner_mut().delegate = delegate; } - /// FIXME: Remove this once we have a webview delegate. pub fn id(&self) -> WebViewId { - self.0.id + self.inner().id + } + + pub fn load_status(&self) -> LoadStatus { + self.inner().load_status + } + + pub(crate) fn set_load_status(self, new_value: LoadStatus) { + if self.inner().load_status == new_value { + return; + } + self.inner_mut().load_status = new_value; + self.delegate().notify_load_status_changed(self, new_value); + } + + pub fn url(&self) -> Option<Url> { + self.inner().url.clone() + } + + pub(crate) fn set_url(self, new_value: Url) { + if self + .inner() + .url + .as_ref() + .is_some_and(|url| url == &new_value) + { + return; + } + self.inner_mut().url = Some(new_value.clone()); + self.delegate().notify_url_changed(self, new_value); + } + + pub fn status_text(&self) -> Option<String> { + self.inner().status_text.clone() + } + + pub(crate) fn set_status_text(self, new_value: Option<String>) { + if self.inner().status_text == new_value { + return; + } + self.inner_mut().status_text = new_value.clone(); + self.delegate().notify_status_text_changed(self, new_value); + } + + pub fn page_title(&self) -> Option<String> { + self.inner().page_title.clone() + } + + pub(crate) fn set_page_title(self, new_value: Option<String>) { + if self.inner().page_title == new_value { + return; + } + self.inner_mut().page_title = new_value.clone(); + self.delegate().notify_page_title_changed(self, new_value); + } + + pub fn favicon_url(&self) -> Option<Url> { + self.inner().favicon_url.clone() + } + + pub(crate) fn set_favicon_url(self, new_value: Url) { + if self + .inner() + .favicon_url + .as_ref() + .is_some_and(|url| url == &new_value) + { + return; + } + self.inner_mut().favicon_url = Some(new_value.clone()); + self.delegate().notify_favicon_url_changed(self, new_value); + } + + pub fn focused(&self) -> bool { + self.inner().focused + } + + pub(crate) fn set_focused(self, new_value: bool) { + if self.inner().focused == new_value { + return; + } + self.inner_mut().focused = new_value; + self.delegate().notify_focus_changed(self, new_value); + } + + pub fn cursor(&self) -> Cursor { + self.inner().cursor + } + + pub(crate) fn set_cursor(self, new_value: Cursor) { + if self.inner().cursor == new_value { + return; + } + self.inner_mut().cursor = new_value; + self.delegate().notify_cursor_changed(self, new_value); } pub fn focus(&self) { - self.0 + self.inner() .constellation_proxy .send(ConstellationMsg::FocusWebView(self.id())); } pub fn blur(&self) { - self.0 + self.inner() .constellation_proxy .send(ConstellationMsg::BlurWebView); } + pub fn rect(&self) -> DeviceRect { + self.inner().rect + } + pub fn move_resize(&self, rect: DeviceRect) { - self.0 + if self.inner().rect == rect { + return; + } + + self.inner_mut().rect = rect; + self.inner() .compositor .borrow_mut() .move_resize_webview(self.id(), rect); } pub fn show(&self, hide_others: bool) { - self.0 + self.inner() .compositor .borrow_mut() .show_webview(self.id(), hide_others) @@ -111,7 +249,7 @@ impl WebView { } pub fn hide(&self) { - self.0 + self.inner() .compositor .borrow_mut() .hide_webview(self.id()) @@ -119,7 +257,7 @@ impl WebView { } pub fn raise_to_top(&self, hide_others: bool) { - self.0 + self.inner() .compositor .borrow_mut() .raise_webview_to_top(self.id(), hide_others) @@ -127,25 +265,25 @@ impl WebView { } pub fn notify_theme_change(&self, theme: Theme) { - self.0 + self.inner() .constellation_proxy .send(ConstellationMsg::ThemeChange(theme)) } pub fn load(&self, url: Url) { - self.0 + self.inner() .constellation_proxy .send(ConstellationMsg::LoadUrl(self.id(), url.into())) } pub fn reload(&self) { - self.0 + self.inner() .constellation_proxy .send(ConstellationMsg::Reload(self.id())) } pub fn go_back(&self, amount: usize) { - self.0 + self.inner() .constellation_proxy .send(ConstellationMsg::TraverseHistory( self.id(), @@ -154,7 +292,7 @@ impl WebView { } pub fn go_forward(&self, amount: usize) { - self.0 + self.inner() .constellation_proxy .send(ConstellationMsg::TraverseHistory( self.id(), @@ -163,28 +301,31 @@ impl WebView { } pub fn notify_pointer_button_event(&self, event: MouseWindowEvent) { - self.0 + self.inner() .compositor .borrow_mut() .on_mouse_window_event_class(event); } pub fn notify_pointer_move_event(&self, event: DevicePoint) { - self.0 + self.inner() .compositor .borrow_mut() .on_mouse_window_move_event_class(event); } pub fn notify_touch_event(&self, event_type: TouchEventType, id: TouchId, point: DevicePoint) { - self.0 + self.inner() .compositor .borrow_mut() .on_touch_event(event_type, id, point); } pub fn notify_wheel_event(&self, delta: WheelDelta, point: DevicePoint) { - self.0.compositor.borrow_mut().on_wheel_event(delta, point); + self.inner() + .compositor + .borrow_mut() + .on_wheel_event(delta, point); } pub fn notify_scroll_event( @@ -193,123 +334,129 @@ impl WebView { point: DeviceIntPoint, touch_event_type: TouchEventType, ) { - self.0 + self.inner() .compositor .borrow_mut() .on_scroll_event(location, point, touch_event_type); } pub fn notify_keyboard_event(&self, event: KeyboardEvent) { - self.0 + self.inner() .constellation_proxy .send(ConstellationMsg::Keyboard(self.id(), event)) } pub fn notify_ime_event(&self, event: CompositionEvent) { - self.0 + self.inner() .constellation_proxy .send(ConstellationMsg::IMECompositionEvent(event)) } pub fn notify_ime_dismissed_event(&self) { - self.0 + self.inner() .constellation_proxy .send(ConstellationMsg::IMEDismissed); } pub fn notify_gamepad_event(&self, event: GamepadEvent) { - self.0 + self.inner() .constellation_proxy .send(ConstellationMsg::Gamepad(event)); } pub fn notify_media_session_action_event(&self, event: MediaSessionActionType) { - self.0 + self.inner() .constellation_proxy .send(ConstellationMsg::MediaSessionAction(event)); } pub fn notify_clipboard_event(&self, event: ClipboardEventType) { - self.0 + self.inner() .constellation_proxy .send(ConstellationMsg::Clipboard(event)); } pub fn notify_vsync(&self) { - self.0.compositor.borrow_mut().on_vsync(); + self.inner().compositor.borrow_mut().on_vsync(); } pub fn notify_rendering_context_resized(&self) { - self.0 + self.inner() .compositor .borrow_mut() .on_rendering_context_resized(); } pub fn set_zoom(&self, new_zoom: f32) { - self.0 + self.inner() .compositor .borrow_mut() .on_zoom_window_event(new_zoom); } pub fn reset_zoom(&self) { - self.0.compositor.borrow_mut().on_zoom_reset_window_event(); + self.inner() + .compositor + .borrow_mut() + .on_zoom_reset_window_event(); } pub fn set_pinch_zoom(&self, new_pinch_zoom: f32) { - self.0 + self.inner() .compositor .borrow_mut() .on_pinch_zoom_window_event(new_pinch_zoom); } pub fn exit_fullscreen(&self) { - self.0 + self.inner() .constellation_proxy .send(ConstellationMsg::ExitFullScreen(self.id())); } pub fn set_throttled(&self, throttled: bool) { - self.0 + self.inner() .constellation_proxy .send(ConstellationMsg::SetWebViewThrottled(self.id(), throttled)); } pub fn toggle_webrender_debugging(&self, debugging: WebRenderDebugOption) { - self.0 + self.inner() .compositor .borrow_mut() .toggle_webrender_debug(debugging); } pub fn capture_webrender(&self) { - self.0.compositor.borrow_mut().capture_webrender(); + self.inner().compositor.borrow_mut().capture_webrender(); } pub fn invalidate_native_surface(&self) { - self.0.compositor.borrow_mut().invalidate_native_surface(); + self.inner() + .compositor + .borrow_mut() + .invalidate_native_surface(); } pub fn composite(&self) { - self.0.compositor.borrow_mut().composite(); + self.inner().compositor.borrow_mut().composite(); } pub fn replace_native_surface(&self, native_widget: *mut c_void, size: DeviceIntSize) { - self.0 + self.inner() .compositor .borrow_mut() .replace_native_surface(native_widget, size); } pub fn toggle_sampling_profiler(&self, rate: Duration, max_duration: Duration) { - self.0 + self.inner() .constellation_proxy .send(ConstellationMsg::ToggleProfiler(rate, max_duration)); } pub fn send_error(&self, message: String) { - self.0 + self.inner() .constellation_proxy .send(ConstellationMsg::SendError(Some(self.id()), message)); } diff --git a/components/servo/webview_delegate.rs b/components/servo/webview_delegate.rs new file mode 100644 index 00000000000..5648294b2c6 --- /dev/null +++ b/components/servo/webview_delegate.rs @@ -0,0 +1,262 @@ +/* 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::path::PathBuf; + +use base::id::PipelineId; +use compositing_traits::ConstellationMsg; +use embedder_traits::{ + CompositorEventVariant, ContextMenuResult, Cursor, FilterPattern, GamepadHapticEffectType, + InputMethodType, LoadStatus, MediaSessionEvent, PermissionPrompt, PermissionRequest, + PromptDefinition, PromptOrigin, WebResourceRequest, WebResourceResponseMsg, +}; +use ipc_channel::ipc::IpcSender; +use keyboard_types::KeyboardEvent; +use url::Url; +use webrender_api::units::{DeviceIntPoint, DeviceIntRect, DeviceIntSize}; + +use crate::{ConstellationProxy, WebView}; + +/// A request to navigate a [`WebView`] or one of its inner frames. This can be handled +/// asynchronously. If not handled, the request will automatically be allowed. +pub struct NavigationRequest { + pub url: Url, + pub(crate) pipeline_id: PipelineId, + pub(crate) constellation_proxy: ConstellationProxy, + pub(crate) response_sent: bool, +} + +impl NavigationRequest { + pub fn allow(mut self) { + self.constellation_proxy + .send(ConstellationMsg::AllowNavigationResponse( + self.pipeline_id, + true, + )); + self.response_sent = true; + } + + pub fn deny(mut self) { + self.constellation_proxy + .send(ConstellationMsg::AllowNavigationResponse( + self.pipeline_id, + false, + )); + self.response_sent = true; + } +} + +impl Drop for NavigationRequest { + fn drop(&mut self) { + if !self.response_sent { + self.constellation_proxy + .send(ConstellationMsg::AllowNavigationResponse( + self.pipeline_id, + true, + )); + } + } +} + +pub struct AllowOrDenyRequest { + pub(crate) response_sender: IpcSender<bool>, + pub(crate) response_sent: bool, + pub(crate) default_response: bool, +} + +impl AllowOrDenyRequest { + pub fn allow(mut self) { + let _ = self.response_sender.send(true); + self.response_sent = true; + } + + pub fn deny(mut self) { + let _ = self.response_sender.send(false); + self.response_sent = true; + } +} + +impl Drop for AllowOrDenyRequest { + fn drop(&mut self) { + if !self.response_sent { + let _ = self.response_sender.send(self.default_response); + } + } +} + +pub trait WebViewDelegate { + /// The URL of the currently loaded page in this [`WebView`] has changed. The new + /// URL can accessed via [`WebView::url`]. + fn notify_url_changed(&self, _webview: WebView, _url: Url) {} + /// The page title of the currently loaded page in this [`WebView`] has changed. The new + /// title can accessed via [`WebView::page_title`]. + fn notify_page_title_changed(&self, _webview: WebView, _title: Option<String>) {} + /// The status text of the currently loaded page in this [`WebView`] has changed. The new + /// status text can accessed via [`WebView::status_text`]. + fn notify_status_text_changed(&self, _webview: WebView, _status: Option<String>) {} + /// This [`WebView`] has either become focused or lost focus. Whether or not the + /// [`WebView`] is focused can be accessed via [`WebView::focused`]. + fn notify_focus_changed(&self, _webview: WebView, _focused: bool) {} + /// The `LoadStatus` of the currently loading or loaded page in this [`WebView`] has changed. The new + /// status can accessed via [`WebView::load_status`]. + fn notify_load_status_changed(&self, _webview: WebView, _status: LoadStatus) {} + /// The [`Cursor`] of the currently loaded page in this [`WebView`] has changed. The new + /// cursor can accessed via [`WebView::cursor`]. + fn notify_cursor_changed(&self, _webview: WebView, _: Cursor) {} + /// The favicon [`Url`] of the currently loaded page in this [`WebView`] has changed. The new + /// favicon [`Url`] can accessed via [`WebView::favicon_url`]. + fn notify_favicon_url_changed(&self, _webview: WebView, _: Url) {} + + /// A [`WebView`] was created and is now ready to show in the user interface. + fn notify_ready_to_show(&self, _webview: WebView) {} + /// Notify the embedder that it needs to present a new frame. + fn notify_new_frame_ready(&self, _webview: WebView) {} + /// The given event was delivered to a pipeline in the given webview. + fn notify_event_delivered(&self, _webview: WebView, _event: CompositorEventVariant) {} + /// The history state has changed. + // changed pattern; maybe wasteful if embedder doesn’t care? + fn notify_history_changed(&self, _webview: WebView, _: Vec<Url>, _: usize) {} + + /// 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`] + /// have an opportunity to handle it first. Apart from builtin keybindings, page + /// content may expose custom keybindings as well. + fn notify_keyboard_event(&self, _webview: WebView, _: KeyboardEvent) {} + /// A pipeline in the webview panicked. First string is the reason, second one is the backtrace. + fn notify_crashed(&self, _webview: WebView, _reason: String, _backtrace: Option<String>) {} + /// Notifies the embedder about media session events + /// (i.e. when there is metadata for the active media session, playback state changes...). + fn notify_media_session_event(&self, _webview: WebView, _event: MediaSessionEvent) {} + + /// Whether or not to allow a [`WebView`] to load a URL in its main frame or one of its + /// nested `<iframe>`s. [`NavigationRequest`]s are accepted by default. + fn request_navigation(&self, _webview: WebView, _navigation_request: NavigationRequest) {} + /// Whether or not to allow a [`WebView`] to unload a `Document` in its main frame or one + /// of its nested `<iframe>`s. By default, unloads are allowed. + fn request_unload(&self, _webview: WebView, _unload_request: AllowOrDenyRequest) {} + /// Move the window to a point + fn request_move_to(&self, _webview: WebView, _: DeviceIntPoint) {} + /// Resize the window to size + fn request_resize_to(&self, _webview: WebView, _: DeviceIntSize) {} + /// Whether or not to allow script to open a new `WebView`. If not handled by the + /// embedder, these requests are automatically denied. + fn request_open_auxiliary_webview(&self, _parent_webview: WebView) -> Option<WebView> { + None + } + /// Page content has requested that this [`WebView`] be closed. It's the embedder's + /// responsibility to either ignore this request or to remove the [`WebView`] from the + /// interface. + fn request_close(&self, _webview: WebView) {} + /// Open interface to request permission specified by prompt. + fn request_permission( + &self, + _webview: WebView, + _: PermissionPrompt, + result_sender: IpcSender<PermissionRequest>, + ) { + let _ = result_sender.send(PermissionRequest::Denied); + } + + /// Show dialog to user + /// TODO: This API needs to be reworked to match the new model of how responses are sent. + fn show_prompt(&self, _webview: WebView, prompt: PromptDefinition, _: PromptOrigin) { + let _ = match prompt { + PromptDefinition::Alert(_, response_sender) => response_sender.send(()), + PromptDefinition::OkCancel(_, response_sender) => { + response_sender.send(embedder_traits::PromptResult::Dismissed) + }, + PromptDefinition::Input(_, _, response_sender) => response_sender.send(None), + PromptDefinition::Credentials(response_sender) => { + response_sender.send(Default::default()) + }, + }; + } + /// Show a context menu to the user + fn show_context_menu( + &self, + _webview: WebView, + result_sender: IpcSender<ContextMenuResult>, + _: Option<String>, + _: Vec<String>, + ) { + let _ = result_sender.send(ContextMenuResult::Ignored); + } + + /// Inform embedder to clear the clipboard + fn clear_clipboard_contents(&self, _webview: WebView) {} + /// Gets system clipboard contents + fn get_clipboard_contents(&self, _webview: WebView, _: IpcSender<String>) {} + /// Sets system clipboard contents + fn set_clipboard_contents(&self, _webview: WebView, _: String) {} + + /// Enter or exit fullscreen + fn request_fullscreen_state_change(&self, _webview: WebView, _: bool) {} + /// Open dialog to select bluetooth device. + /// TODO: This API needs to be reworked to match the new model of how responses are sent. + fn show_bluetooth_device_dialog( + &self, + _webview: WebView, + _: Vec<String>, + response_sender: IpcSender<Option<String>>, + ) { + let _ = response_sender.send(None); + } + + /// Open file dialog to select files. Set boolean flag to true allows to select multiple files. + fn show_file_selection_dialog( + &self, + _webview: WebView, + _filter_pattern: Vec<FilterPattern>, + _allow_select_mutiple: bool, + response_sender: IpcSender<Option<Vec<PathBuf>>>, + ) { + let _ = response_sender.send(None); + } + + /// Request to present an IME to the user when an editable element is focused. + /// If `type` is [`InputMethodType::Text`], then the `text` parameter specifies + /// the pre-existing text content and the zero-based index into the string + /// of the insertion point. + fn show_ime( + &self, + _webview: WebView, + _type: InputMethodType, + _text: Option<(String, i32)>, + _multiline: bool, + _position: DeviceIntRect, + ) { + } + + /// Request to hide the IME when the editable element is blurred. + fn hide_ime(&self, _webview: WebView) {} + + /// Request to play a haptic effect on a connected gamepad. + fn play_gamepad_haptic_effect( + &self, + _webview: WebView, + _: usize, + _: GamepadHapticEffectType, + _: IpcSender<bool>, + ) { + } + /// Request to stop a haptic effect on a connected gamepad. + fn stop_gamepad_haptic_effect(&self, _webview: WebView, _: usize, _: IpcSender<bool>) {} + + /// Potentially intercept a resource request. If not handled, the request will not be intercepted. + /// + /// Note: The `ServoDelegate` will also receive this notification and have a chance to intercept + /// the request. + /// + /// TODO: This API needs to be reworked to match the new model of how responses are sent. + fn intercept_web_resource_load( + &self, + _webview: WebView, + _request: &WebResourceRequest, + _response_sender: IpcSender<WebResourceResponseMsg>, + ) { + } +} + +pub(crate) struct DefaultWebViewDelegate; +impl WebViewDelegate for DefaultWebViewDelegate {} diff --git a/components/shared/embedder/lib.rs b/components/shared/embedder/lib.rs index dca4fa65abd..1ba259274e6 100644 --- a/components/shared/embedder/lib.rs +++ b/components/shared/embedder/lib.rs @@ -114,15 +114,13 @@ pub enum PromptDefinition { Alert(String, IpcSender<()>), /// Ask a Ok/Cancel question. OkCancel(String, IpcSender<PromptResult>), - /// Ask a Yes/No question. - YesNo(String, IpcSender<PromptResult>), /// Ask the user to enter text. Input(String, String, IpcSender<Option<String>>), /// Ask user to enter their username and password Credentials(IpcSender<PromptCredentialsInput>), } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Default, Deserialize, Serialize)] pub struct PromptCredentialsInput { /// Username for http request authentication pub username: Option<String>, diff --git a/ports/servoshell/desktop/app.rs b/ports/servoshell/desktop/app.rs index 9643a1835f2..1457bc5dc99 100644 --- a/ports/servoshell/desktop/app.rs +++ b/ports/servoshell/desktop/app.rs @@ -249,7 +249,7 @@ impl App { // Take any new embedder messages from Servo. let servo = self.servo.as_mut().expect("Servo should be running."); - let mut embedder_messages: Vec<_> = servo.get_events().collect(); + let mut embedder_messages = servo.get_events(); let mut need_present = false; let mut need_update = false; loop { @@ -271,7 +271,7 @@ impl App { } // Take any new embedder messages from Servo itself. - embedder_messages = servo.get_events().collect(); + embedder_messages = servo.get_events(); if embedder_messages.is_empty() { break; } diff --git a/ports/servoshell/desktop/webview.rs b/ports/servoshell/desktop/webview.rs index 2d6a7e57d87..f4d615a1ef2 100644 --- a/ports/servoshell/desktop/webview.rs +++ b/ports/servoshell/desktop/webview.rs @@ -28,7 +28,7 @@ use servo::{ PermissionRequest, PromptCredentialsInput, PromptDefinition, PromptOrigin, PromptResult, Servo, TouchEventType, }; -use tinyfiledialogs::{self, MessageBoxIcon, OkCancel, YesNo}; +use tinyfiledialogs::{self, MessageBoxIcon, OkCancel}; use super::dialog::Dialog; use super::keyutils::CMD_OR_CONTROL; @@ -487,9 +487,6 @@ impl WebViewManager { let res = if opts.headless { match definition { PromptDefinition::Alert(_message, sender) => sender.send(()), - PromptDefinition::YesNo(_message, sender) => { - sender.send(PromptResult::Primary) - }, PromptDefinition::OkCancel(_message, sender) => { sender.send(PromptResult::Primary) }, @@ -518,21 +515,6 @@ impl WebViewManager { ); sender.send(()) }, - PromptDefinition::YesNo(mut message, sender) => { - if origin == PromptOrigin::Untrusted { - message = tiny_dialog_escape(&message); - } - let result = tinyfiledialogs::message_box_yes_no( - "", - &message, - MessageBoxIcon::Warning, - YesNo::No, - ); - sender.send(match result { - YesNo::Yes => PromptResult::Primary, - YesNo::No => PromptResult::Secondary, - }) - }, PromptDefinition::OkCancel(mut message, sender) => { if origin == PromptOrigin::Untrusted { message = tiny_dialog_escape(&message); @@ -824,6 +806,8 @@ impl WebViewManager { #[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) diff --git a/ports/servoshell/egl/servo_glue.rs b/ports/servoshell/egl/servo_glue.rs index 2d9f9db1b41..ca1397f6747 100644 --- a/ports/servoshell/egl/servo_glue.rs +++ b/ports/servoshell/egl/servo_glue.rs @@ -455,7 +455,7 @@ impl ServoGlue { fn handle_servo_events(&mut self) -> Result<(), &'static str> { let mut need_update = false; - let messages: Vec<_> = self.servo.get_events().collect(); + let messages = self.servo.get_events(); for message in messages { match message { EmbedderMsg::ChangePageTitle(_, title) => { @@ -514,9 +514,6 @@ impl ServoGlue { PromptDefinition::OkCancel(message, sender) => { sender.send(cb.prompt_ok_cancel(message, trusted)) }, - PromptDefinition::YesNo(message, sender) => { - sender.send(cb.prompt_yes_no(message, trusted)) - }, PromptDefinition::Input(message, default, sender) => { sender.send(cb.prompt_input(message, default, trusted)) }, |