From 9dbc942beeab1ce9d771706ee3c6d09c6b7aa227 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Tue, 28 Jan 2025 15:57:57 +0100 Subject: [PATCH] libservo: Port desktop servoshell to use the new `WebView` API (#35183) This removes all uses of `EmbedderEvent` in the desktop servoshell to use the new `WebView` API -- filling it out when necessary. Signed-off-by: Martin Robinson Co-authored-by: Delan Azabani --- .../background_hang_monitor.rs | 22 +- components/compositing/compositor.rs | 2 +- components/constellation/constellation.rs | 13 +- components/constellation/tracing.rs | 3 +- components/servo/lib.rs | 31 +- components/servo/webview.rs | 199 +++++- .../shared/background_hang_monitor/lib.rs | 5 +- .../shared/compositing/constellation_msg.rs | 7 +- ports/servoshell/desktop/app.rs | 173 ++--- ports/servoshell/desktop/headed_window.rs | 289 +++++--- ports/servoshell/desktop/headless_window.rs | 36 +- ports/servoshell/desktop/minibrowser.rs | 130 +--- ports/servoshell/desktop/tracing.rs | 74 +-- ports/servoshell/desktop/webview.rs | 620 ++++++------------ ports/servoshell/desktop/window_trait.rs | 20 +- 15 files changed, 826 insertions(+), 798 deletions(-) diff --git a/components/background_hang_monitor/background_hang_monitor.rs b/components/background_hang_monitor/background_hang_monitor.rs index cdc077a38ae..9a034e6e0b9 100644 --- a/components/background_hang_monitor/background_hang_monitor.rs +++ b/components/background_hang_monitor/background_hang_monitor.rs @@ -427,19 +427,19 @@ impl BackgroundHangMonitorWorker { }, recv(self.control_port) -> event => { match event { - Ok(BackgroundHangMonitorControlMsg::EnableSampler(rate, max_duration)) => { - println!("Enabling profiler."); - self.sampling_duration = Some(rate); - self.sampling_max_duration = Some(max_duration); - self.sampling_baseline = Instant::now(); + Ok(BackgroundHangMonitorControlMsg::ToggleSampler(rate, max_duration)) => { + if self.sampling_duration.is_some() { + println!("Enabling profiler."); + self.finish_sampled_profile(); + self.sampling_duration = None; + } else { + println!("Disabling profiler."); + self.sampling_duration = Some(rate); + self.sampling_max_duration = Some(max_duration); + self.sampling_baseline = Instant::now(); + } None }, - Ok(BackgroundHangMonitorControlMsg::DisableSampler) => { - println!("Disabling profiler."); - self.finish_sampled_profile(); - self.sampling_duration = None; - return true; - }, Ok(BackgroundHangMonitorControlMsg::Exit(sender)) => { for component in self.monitored_components.values_mut() { component.exit_signal.signal_to_exit(); diff --git a/components/compositing/compositor.rs b/components/compositing/compositor.rs index 85c17ca96a1..5bb5eeef9f3 100644 --- a/components/compositing/compositor.rs +++ b/components/compositing/compositor.rs @@ -1318,7 +1318,7 @@ impl IOCompositor { self.pipeline_details.remove(&pipeline_id); } - pub fn on_resize_window_event(&mut self) -> bool { + pub fn on_rendering_context_resized(&mut self) -> bool { if self.shutdown_state != ShutdownState::NotShuttingDown { return false; } diff --git a/components/constellation/constellation.rs b/components/constellation/constellation.rs index 54360b8cd42..d25880efefc 100644 --- a/components/constellation/constellation.rs +++ b/components/constellation/constellation.rs @@ -1443,24 +1443,15 @@ where self.forward_event(destination_pipeline_id, event); }, FromCompositorMsg::SetCursor(cursor) => self.handle_set_cursor_msg(cursor), - FromCompositorMsg::EnableProfiler(rate, max_duration) => { + FromCompositorMsg::ToggleProfiler(rate, max_duration) => { for background_monitor_control_sender in &self.background_monitor_control_senders { if let Err(e) = background_monitor_control_sender.send( - BackgroundHangMonitorControlMsg::EnableSampler(rate, max_duration), + BackgroundHangMonitorControlMsg::ToggleSampler(rate, max_duration), ) { warn!("error communicating with sampling profiler: {}", e); } } }, - FromCompositorMsg::DisableProfiler => { - for background_monitor_control_sender in &self.background_monitor_control_senders { - if let Err(e) = background_monitor_control_sender - .send(BackgroundHangMonitorControlMsg::DisableSampler) - { - warn!("error communicating with sampling profiler: {}", e); - } - } - }, FromCompositorMsg::ExitFullScreen(top_level_browsing_context_id) => { self.handle_exit_fullscreen_msg(top_level_browsing_context_id); }, diff --git a/components/constellation/tracing.rs b/components/constellation/tracing.rs index 6f90737c905..f0f988d163b 100644 --- a/components/constellation/tracing.rs +++ b/components/constellation/tracing.rs @@ -85,8 +85,7 @@ mod from_compositor { Self::BlurWebView => target!("BlurWebView"), Self::ForwardEvent(_, event) => event.log_target(), Self::SetCursor(..) => target!("SetCursor"), - Self::EnableProfiler(..) => target!("EnableProfiler"), - Self::DisableProfiler => target!("DisableProfiler"), + Self::ToggleProfiler(..) => target!("EnableProfiler"), Self::ExitFullScreen(_) => target!("ExitFullScreen"), Self::MediaSessionAction(_) => target!("MediaSessionAction"), Self::SetWebViewThrottled(_, _) => target!("SetWebViewThrottled"), diff --git a/components/servo/lib.rs b/components/servo/lib.rs index 5f3d45076be..cc0047aa817 100644 --- a/components/servo/lib.rs +++ b/components/servo/lib.rs @@ -30,7 +30,7 @@ use std::thread; use std::vec::Drain; pub use base::id::TopLevelBrowsingContextId; -use base::id::{PipelineNamespace, PipelineNamespaceId}; +use base::id::{PipelineId, PipelineNamespace, PipelineNamespaceId}; use bluetooth::BluetoothThreadFactory; use bluetooth_traits::BluetoothRequest; use canvas::canvas_paint_thread::CanvasPaintThread; @@ -189,7 +189,6 @@ pub struct Servo { constellation_proxy: ConstellationProxy, embedder_receiver: EmbedderReceiver, messages_for_embedder: Vec<(Option, EmbedderMsg)>, - profiler_enabled: bool, /// 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. @@ -530,7 +529,6 @@ impl Servo { constellation_proxy: ConstellationProxy::new(constellation_chan), embedder_receiver, messages_for_embedder: Vec::new(), - profiler_enabled: false, _js_engine_setup: js_engine_setup, } } @@ -652,7 +650,7 @@ impl Servo { }, EmbedderEvent::WindowResize => { - return self.compositor.borrow_mut().on_resize_window_event(); + return self.compositor.borrow_mut().on_rendering_context_resized(); }, EmbedderEvent::ThemeChange(theme) => { let msg = ConstellationMsg::ThemeChange(theme); @@ -798,13 +796,10 @@ impl Servo { }, EmbedderEvent::ToggleSamplingProfiler(rate, max_duration) => { - self.profiler_enabled = !self.profiler_enabled; - let msg = if self.profiler_enabled { - ConstellationMsg::EnableProfiler(rate, max_duration) - } else { - ConstellationMsg::DisableProfiler - }; - if let Err(e) = self.constellation_proxy.try_send(msg) { + if let Err(e) = self + .constellation_proxy + .try_send(ConstellationMsg::ToggleProfiler(rate, max_duration)) + { warn!("Sending profiler toggle to constellation failed ({:?}).", e); } }, @@ -1003,6 +998,20 @@ impl Servo { log::set_max_level(filter); } + pub fn start_shutting_down(&self) { + self.compositor.borrow_mut().maybe_start_shutting_down(); + } + + pub fn allow_navigation_response(&self, pipeline_id: PipelineId, allow: bool) { + let msg = ConstellationMsg::AllowNavigationResponse(pipeline_id, allow); + if let Err(e) = self.constellation_proxy.try_send(msg) { + warn!( + "Sending allow navigation to constellation failed ({:?}).", + e + ); + } + } + pub fn deinit(self) { self.compositor.borrow_mut().deinit(); } diff --git a/components/servo/webview.rs b/components/servo/webview.rs index 03dd954f688..412ea67e6a0 100644 --- a/components/servo/webview.rs +++ b/components/servo/webview.rs @@ -3,15 +3,26 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use std::cell::RefCell; +use std::ffi::c_void; use std::rc::Rc; +use std::time::Duration; use base::id::WebViewId; +use compositing::windowing::{MouseWindowEvent, WebRenderDebugOption}; use compositing::IOCompositor; use compositing_traits::ConstellationMsg; -use webrender_api::units::DeviceRect; +use embedder_traits::{ + ClipboardEventType, GamepadEvent, 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::ConstellationProxy; +#[derive(Clone)] pub struct WebView(Rc); struct WebViewInner { @@ -38,7 +49,7 @@ impl WebView { pub(crate) fn new( constellation_proxy: &ConstellationProxy, compositor: Rc>, - url: url::Url, + url: Url, ) -> Self { let webview_id = WebViewId::new(); constellation_proxy.send(ConstellationMsg::NewWebView(url.into(), webview_id)); @@ -114,4 +125,188 @@ impl WebView { .raise_webview_to_top(self.id(), hide_others) .expect("BUG: invalid WebView instance"); } + + pub fn notify_theme_change(&self, theme: Theme) { + self.0 + .constellation_proxy + .send(ConstellationMsg::ThemeChange(theme)) + } + + pub fn load(&self, url: Url) { + self.0 + .constellation_proxy + .send(ConstellationMsg::LoadUrl(self.id(), url.into())) + } + + pub fn reload(&self) { + self.0 + .constellation_proxy + .send(ConstellationMsg::Reload(self.id())) + } + + pub fn go_back(&self, amount: usize) { + self.0 + .constellation_proxy + .send(ConstellationMsg::TraverseHistory( + self.id(), + TraversalDirection::Back(amount), + )) + } + + pub fn go_forward(&self, amount: usize) { + self.0 + .constellation_proxy + .send(ConstellationMsg::TraverseHistory( + self.id(), + TraversalDirection::Forward(amount), + )) + } + + pub fn notify_pointer_button_event(&self, event: MouseWindowEvent) { + self.0 + .compositor + .borrow_mut() + .on_mouse_window_event_class(event); + } + + pub fn notify_pointer_move_event(&self, event: DevicePoint) { + self.0 + .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 + .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); + } + + pub fn notify_scroll_event( + &self, + location: ScrollLocation, + point: DeviceIntPoint, + touch_event_type: TouchEventType, + ) { + self.0 + .compositor + .borrow_mut() + .on_scroll_event(location, point, touch_event_type); + } + + pub fn notify_keyboard_event(&self, event: KeyboardEvent) { + self.0 + .constellation_proxy + .send(ConstellationMsg::Keyboard(event)) + } + + pub fn notify_ime_event(&self, event: CompositionEvent) { + self.0 + .constellation_proxy + .send(ConstellationMsg::IMECompositionEvent(event)) + } + + pub fn notify_ime_dismissed_event(&self) { + self.0 + .constellation_proxy + .send(ConstellationMsg::IMEDismissed); + } + + pub fn notify_gamepad_event(&self, event: GamepadEvent) { + self.0 + .constellation_proxy + .send(ConstellationMsg::Gamepad(event)); + } + + pub fn notify_media_session_action_event(&self, event: MediaSessionActionType) { + self.0 + .constellation_proxy + .send(ConstellationMsg::MediaSessionAction(event)); + } + + pub fn notify_clipboard_event(&self, event: ClipboardEventType) { + self.0 + .constellation_proxy + .send(ConstellationMsg::Clipboard(event)); + } + + pub fn notify_vsync(&self) { + self.0.compositor.borrow_mut().on_vsync(); + } + + pub fn notify_rendering_context_resized(&self) { + self.0 + .compositor + .borrow_mut() + .on_rendering_context_resized(); + } + + pub fn set_zoom(&self, new_zoom: f32) { + self.0 + .compositor + .borrow_mut() + .on_zoom_window_event(new_zoom); + } + + pub fn reset_zoom(&self) { + self.0.compositor.borrow_mut().on_zoom_reset_window_event(); + } + + pub fn set_pinch_zoom(&self, new_pinch_zoom: f32) { + self.0 + .compositor + .borrow_mut() + .on_pinch_zoom_window_event(new_pinch_zoom); + } + + pub fn exit_fullscreen(&self) { + self.0 + .constellation_proxy + .send(ConstellationMsg::ExitFullScreen(self.id())); + } + + pub fn set_throttled(&self, throttled: bool) { + self.0 + .constellation_proxy + .send(ConstellationMsg::SetWebViewThrottled(self.id(), throttled)); + } + + pub fn toggle_webrender_debugging(&self, debugging: WebRenderDebugOption) { + self.0 + .compositor + .borrow_mut() + .toggle_webrender_debug(debugging); + } + + pub fn capture_webrender(&self) { + self.0.compositor.borrow_mut().capture_webrender(); + } + + pub fn invalidate_native_surface(&self) { + self.0.compositor.borrow_mut().invalidate_native_surface(); + } + + pub fn replace_native_surface(&self, native_widget: *mut c_void, size: DeviceIntSize) { + self.0 + .compositor + .borrow_mut() + .replace_native_surface(native_widget, size); + } + + pub fn toggle_sampling_profiler(&self, rate: Duration, max_duration: Duration) { + self.0 + .constellation_proxy + .send(ConstellationMsg::ToggleProfiler(rate, max_duration)); + } + + pub fn send_error(&self, message: String) { + self.0 + .constellation_proxy + .send(ConstellationMsg::SendError(Some(self.id()), message)); + } } diff --git a/components/shared/background_hang_monitor/lib.rs b/components/shared/background_hang_monitor/lib.rs index 145aa9ea0c9..d8118a8808f 100644 --- a/components/shared/background_hang_monitor/lib.rs +++ b/components/shared/background_hang_monitor/lib.rs @@ -205,9 +205,8 @@ pub trait BackgroundHangMonitorExitSignal: Send { /// Messages to control the sampling profiler. #[derive(Deserialize, Serialize)] pub enum BackgroundHangMonitorControlMsg { - /// Enable the sampler, with a given sampling rate and max total sampling duration. - EnableSampler(Duration, Duration), - DisableSampler, + /// Toggle the sampler, with a given sampling rate and max total sampling duration. + ToggleSampler(Duration, Duration), /// Exit, and propagate the signal to monitored components. Exit(IpcSender<()>), } diff --git a/components/shared/compositing/constellation_msg.rs b/components/shared/compositing/constellation_msg.rs index cf251e3d977..9f1d345f043 100644 --- a/components/shared/compositing/constellation_msg.rs +++ b/components/shared/compositing/constellation_msg.rs @@ -75,9 +75,7 @@ pub enum ConstellationMsg { /// Requesting a change to the onscreen cursor. SetCursor(Cursor), /// Enable the sampling profiler, with a given sampling rate and max total sampling duration. - EnableProfiler(Duration, Duration), - /// Disable the sampling profiler. - DisableProfiler, + ToggleProfiler(Duration, Duration), /// Request to exit from fullscreen mode ExitFullScreen(TopLevelBrowsingContextId), /// Media session action. @@ -129,8 +127,7 @@ impl ConstellationMsg { SendError(..) => "SendError", ForwardEvent(..) => "ForwardEvent", SetCursor(..) => "SetCursor", - EnableProfiler(..) => "EnableProfiler", - DisableProfiler => "DisableProfiler", + ToggleProfiler(..) => "EnableProfiler", ExitFullScreen(..) => "ExitFullScreen", MediaSessionAction(..) => "MediaSessionAction", SetWebViewThrottled(..) => "SetWebViewThrottled", diff --git a/ports/servoshell/desktop/app.rs b/ports/servoshell/desktop/app.rs index 73b0b00acdb..c9b30ab669e 100644 --- a/ports/servoshell/desktop/app.rs +++ b/ports/servoshell/desktop/app.rs @@ -10,10 +10,10 @@ use std::rc::Rc; use std::time::Instant; use std::{env, fs}; -use log::{info, trace}; +use arboard::Clipboard; +use log::{info, trace, warn}; use raw_window_handle::HasDisplayHandle; -use servo::base::id::WebViewId; -use servo::compositing::windowing::{AnimationState, EmbedderEvent, WindowMethods}; +use servo::compositing::windowing::{AnimationState, WindowMethods}; use servo::compositing::CompositeTarget; use servo::config::opts::Opts; use servo::config::prefs::Preferences; @@ -22,6 +22,7 @@ use servo::servo_url::ServoUrl; use servo::webrender_traits::SurfmanRenderingContext; use servo::{EventLoopWaker, Servo}; use surfman::Connection; +use url::Url; use webxr::glwindow::GlWindowDiscovery; #[cfg(target_os = "windows")] use webxr::openxr::{AppInfo, OpenXrDiscovery}; @@ -31,22 +32,22 @@ use winit::event_loop::{ActiveEventLoop, ControlFlow}; use winit::window::WindowId; use super::events_loop::{EventsLoop, WakerEvent}; -use super::minibrowser::Minibrowser; +use super::minibrowser::{Minibrowser, MinibrowserEvent}; use super::webview::WebViewManager; use super::{headed_window, headless_window}; use crate::desktop::embedder::{EmbedderCallbacks, XrDiscovery}; use crate::desktop::tracing::trace_winit_event; use crate::desktop::window_trait::WindowPortsMethods; -use crate::parser::get_default_url; +use crate::parser::{get_default_url, location_bar_input_to_url}; use crate::prefs::ServoShellPreferences; pub struct App { opts: Opts, preferences: Preferences, servo_shell_preferences: ServoShellPreferences, + clipboard: Option, servo: Option, - webviews: Option>, - event_queue: Vec, + webviews: Option, suspended: Cell, windows: HashMap>, minibrowser: Option, @@ -91,7 +92,7 @@ impl App { opts, preferences, servo_shell_preferences, - event_queue: vec![], + clipboard: Clipboard::new().ok(), webviews: None, servo: None, suspended: Cell::new(false), @@ -164,7 +165,7 @@ impl App { minibrowser.update( window.winit_window().unwrap(), self.webviews.as_mut().unwrap(), - None, + self.servo.as_mut(), "init", ); window.set_toolbar_height(minibrowser.toolbar_height); @@ -173,7 +174,6 @@ impl App { self.windows.insert(window.id(), window); self.suspended.set(false); - self.event_queue.push(EmbedderEvent::Idle); let (_, window) = self.windows.iter().next().unwrap(); let xr_discovery = if pref!(dom_webxr_openxr_enabled) && !self.opts.headless { @@ -213,7 +213,7 @@ impl App { } else { CompositeTarget::Window }; - let mut servo = Servo::new( + let servo = Servo::new( self.opts.clone(), self.preferences.clone(), Rc::new(rendering_context), @@ -223,12 +223,11 @@ impl App { composite_target, ); - servo.handle_events(vec![EmbedderEvent::NewWebView( - self.initial_url.to_owned(), - WebViewId::new(), - )]); servo.setup_logging(); + let webview = servo.new_webview(self.initial_url.clone().into_url()); + self.webviews.as_mut().unwrap().add(webview); + self.servo = Some(servo); } @@ -236,61 +235,47 @@ impl App { self.windows.iter().any(|(_, window)| window.is_animating()) } - fn get_events(&mut self) -> Vec { - std::mem::take(&mut self.event_queue) - } - - /// Pumps events and messages between the embedder and Servo, where embedder events flow - /// towards Servo and embedder messages flow away from Servo, and also runs the compositor. + /// 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 /// - /// As the embedder, we push embedder events through our event queues, from the App queue and - /// Window queues to the WebViewManager queue, and from the WebViewManager queue to Servo. We - /// receive and collect embedder messages from the various Servo components, then take them out - /// of the Servo interface so that the WebViewManager can handle them. + /// In the future, these tasks may be decoupled. fn handle_events(&mut self) -> PumpResult { - let mut embedder_events = self.get_events(); let webviews = self.webviews.as_mut().unwrap(); - // Take any outstanding embedder events from the App and its Windows. - for window in self.windows.values() { - embedder_events.extend(window.get_events()); - } - - // Catch some keyboard events, and push the rest onto the WebViewManager event queue. - webviews.handle_window_events(embedder_events); - // 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) && webviews.focused_webview_id().is_some() { webviews.handle_gamepad_events(); } - // Take any new embedder messages from Servo itself. - let mut embedder_messages = self.servo.as_mut().unwrap().get_events(); + // 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 need_resize = false; let mut need_present = false; let mut need_update = false; loop { // Consume and handle those embedder messages. - let servo_event_response = webviews.handle_servo_events(&self.opts, embedder_messages); + let servo_event_response = 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; - // Route embedder events from the WebViewManager to the relevant Servo components, - // receives and collects embedder messages from various Servo components, - // and runs the compositor. - need_resize |= self - .servo - .as_mut() - .unwrap() - .handle_events(webviews.get_events()); + // Runs the compositor, and receives and collects embedder messages from various Servo components. + need_resize |= servo.handle_events(vec![]); if webviews.shutdown_requested() { return PumpResult::Shutdown; } // Take any new embedder messages from Servo itself. - embedder_messages = self.servo.as_mut().unwrap().get_events(); - if embedder_messages.len() == 0 { + embedder_messages = servo.get_events().collect(); + if embedder_messages.is_empty() { break; } } @@ -333,7 +318,7 @@ impl App { minibrowser.update( window.winit_window().unwrap(), webviews, - self.servo.as_ref().unwrap().offscreen_framebuffer_id(), + self.servo.as_mut(), "update_location_in_toolbar", ); } @@ -351,7 +336,7 @@ impl App { minibrowser.update( window.winit_window().unwrap(), self.webviews.as_mut().unwrap(), - self.servo.as_ref().unwrap().offscreen_framebuffer_id(), + self.servo.as_mut(), "PumpResult::Present::Immediate", ); minibrowser.paint(window.winit_window().unwrap()); @@ -391,7 +376,6 @@ impl App { if self.servo.is_none() { return false; } - self.event_queue.push(EmbedderEvent::Idle); let mut exit = false; match self.handle_events() { @@ -421,6 +405,62 @@ impl App { } exit } + + /// Takes any events generated during `egui` updates and performs their actions. + fn handle_servoshell_ui_events(&mut self) { + let Some(webviews) = self.webviews.as_mut() else { + return; + }; + let Some(minibrowser) = self.minibrowser.as_ref() else { + return; + }; + let Some(servo) = self.servo.as_ref() else { + return; + }; + + for event in minibrowser.take_events() { + match event { + MinibrowserEvent::Go(location) => { + minibrowser.update_location_dirty(false); + let Some(url) = location_bar_input_to_url( + &location.clone(), + &self.servo_shell_preferences.searchpage, + ) else { + warn!("failed to parse location"); + break; + }; + if let Some(focused_webview) = webviews.focused_webview() { + focused_webview.servo_webview.load(url.into_url()); + } + }, + MinibrowserEvent::Back => { + if let Some(focused_webview) = webviews.focused_webview() { + focused_webview.servo_webview.go_back(1); + } + }, + MinibrowserEvent::Forward => { + if let Some(focused_webview) = webviews.focused_webview() { + focused_webview.servo_webview.go_forward(1); + } + }, + MinibrowserEvent::Reload => { + minibrowser.update_location_dirty(false); + if let Some(focused_webview) = webviews.focused_webview() { + focused_webview.servo_webview.reload(); + } + }, + MinibrowserEvent::NewWebView => { + minibrowser.update_location_dirty(false); + let webview = servo.new_webview(Url::parse("servo:newtab").unwrap()); + webviews.add(webview); + }, + MinibrowserEvent::CloseWebView(id) => { + minibrowser.update_location_dirty(false); + webviews.close_webview(servo, id); + }, + } + } + } } impl ApplicationHandler for App { @@ -463,7 +503,7 @@ impl ApplicationHandler for App { minibrowser.update( window.winit_window().unwrap(), self.webviews.as_mut().unwrap(), - self.servo.as_ref().unwrap().offscreen_framebuffer_id(), + self.servo.as_mut(), "RedrawRequested", ); minibrowser.paint(window.winit_window().unwrap()); @@ -505,7 +545,7 @@ impl ApplicationHandler for App { minibrowser.update( window.winit_window().unwrap(), self.webviews.as_mut().unwrap(), - self.servo.as_ref().unwrap().offscreen_framebuffer_id(), + self.servo.as_mut(), "Sync WebView size with Window Resize event", ); } @@ -522,11 +562,9 @@ impl ApplicationHandler for App { } } if !consumed { - if event == winit::event::WindowEvent::RedrawRequested { - self.event_queue.push(EmbedderEvent::Idle); + if let (Some(servo), Some(webviews)) = (self.servo.as_ref(), self.webviews.as_mut()) { + window.handle_winit_event(servo, &mut self.clipboard, webviews, event); } - - window.queue_embedder_events_for_winit_event(event); } let animating = self.is_animating(); @@ -538,16 +576,8 @@ impl ApplicationHandler for App { event_loop.set_control_flow(ControlFlow::Poll); } - // Consume and handle any events from the Minibrowser. - if let Some(ref minibrowser) = self.minibrowser { - let webviews = &mut self.webviews.as_mut().unwrap(); - let app_event_queue = &mut self.event_queue; - minibrowser.queue_embedder_events_for_minibrowser_events( - webviews, - app_event_queue, - &self.servo_shell_preferences, - ); - } + // Consume and handle any events from the servoshell UI. + self.handle_servoshell_ui_events(); self.handle_events_with_winit(event_loop, window); } @@ -567,7 +597,6 @@ impl ApplicationHandler for App { if self.servo.is_none() { return; } - self.event_queue.push(EmbedderEvent::Idle); let Some(window) = self.windows.values().next() else { return; @@ -584,15 +613,7 @@ impl ApplicationHandler for App { } // Consume and handle any events from the Minibrowser. - if let Some(ref minibrowser) = self.minibrowser { - let webviews = &mut self.webviews.as_mut().unwrap(); - let app_event_queue = &mut self.event_queue; - minibrowser.queue_embedder_events_for_minibrowser_events( - webviews, - app_event_queue, - &self.servo_shell_preferences, - ); - } + self.handle_servoshell_ui_events(); self.handle_events_with_winit(event_loop, window); } diff --git a/ports/servoshell/desktop/headed_window.rs b/ports/servoshell/desktop/headed_window.rs index ec3c9180b0f..798519b17c3 100644 --- a/ports/servoshell/desktop/headed_window.rs +++ b/ports/servoshell/desktop/headed_window.rs @@ -6,13 +6,17 @@ use std::cell::{Cell, RefCell}; use std::collections::HashMap; +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 log::{debug, info, trace}; +use keyboard_types::{Modifiers, ShortcutMatcher}; +use log::{debug, info}; use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; use servo::compositing::windowing::{ - AnimationState, EmbedderCoordinates, EmbedderEvent, MouseWindowEvent, WindowMethods, + AnimationState, EmbedderCoordinates, MouseWindowEvent, WebRenderDebugOption, WindowMethods, }; use servo::config::opts::Opts; use servo::servo_config::pref; @@ -21,10 +25,11 @@ use servo::webrender_api::units::{DeviceIntPoint, DeviceIntRect, DeviceIntSize, use servo::webrender_api::ScrollLocation; use servo::webrender_traits::SurfmanRenderingContext; use servo::{ - Cursor, Key, KeyState, KeyboardEvent, MouseButton as ServoMouseButton, Theme, TouchEventType, - TouchId, WheelDelta, WheelMode, + ClipboardEventType, Cursor, Key, KeyState, KeyboardEvent, MouseButton as ServoMouseButton, + Servo, Theme, TouchEventType, TouchId, WebView, WheelDelta, WheelMode, }; use surfman::{Context, Device, SurfaceType}; +use url::Url; use winit::dpi::{LogicalSize, PhysicalPosition, PhysicalSize}; use winit::event::{ElementState, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase}; use winit::event_loop::ActiveEventLoop; @@ -33,8 +38,10 @@ use winit::keyboard::{Key as LogicalKey, ModifiersState, NamedKey}; use winit::window::Icon; use super::geometry::{winit_position_to_euclid_point, winit_size_to_euclid_size}; -use super::keyutils::keyboard_event_from_winit; +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; pub struct Window { winit_window: winit::window::Window, @@ -44,7 +51,6 @@ pub struct Window { mouse_down_button: Cell>, mouse_down_point: Cell>, monitor: winit::monitor::MonitorHandle, - event_queue: RefCell>, mouse_pos: Cell>, last_pressed: Cell)>>, /// A map of winit's key codes to key values that are interpreted from @@ -130,7 +136,6 @@ impl Window { debug!("Created window {:?}", winit_window.id()); Window { winit_window, - event_queue: RefCell::new(vec![]), mouse_down_button: Cell::new(None), mouse_down_point: Cell::new(Point2D::zero()), mouse_pos: Cell::new(Point2D::zero()), @@ -148,19 +153,19 @@ impl Window { } } - fn handle_received_character(&self, mut ch: char) { - info!("winit received character: {:?}", ch); - if ch.is_control() { - if ch as u8 >= 32 { + fn handle_received_character(&self, webview: &WebView, mut character: char) { + info!("winit received character: {:?}", character); + if character.is_control() { + if character as u8 >= 32 { return; } // shift ASCII control characters to lowercase - ch = (ch as u8 + 96) as char; + character = (character as u8 + 96) as char; } let (mut event, key_code) = if let Some((event, key_code)) = self.last_pressed.replace(None) { (event, key_code) - } else if ch.is_ascii() { + } else if character.is_ascii() { // Some keys like Backspace emit a control character in winit // but they are already dealt with in handle_keyboard_input // so just ignore the character. @@ -170,7 +175,7 @@ impl Window { // no keyboard event is emitted. A dummy event is created in this case. (KeyboardEvent::default(), None) }; - event.key = Key::Character(ch.to_string()); + event.key = Key::Character(character.to_string()); if event.state == KeyState::Down { // Ensure that when we receive a keyup event from winit, we are able @@ -187,50 +192,69 @@ impl Window { for xr_window_pose in &*xr_poses { xr_window_pose.handle_xr_translation(&event); } - self.event_queue - .borrow_mut() - .push(EmbedderEvent::Keyboard(event)); + webview.notify_keyboard_event(event); } - fn handle_keyboard_input(&self, input: KeyEvent) { - if let Some(input_text) = &input.text { - for ch in input_text.chars() { - self.handle_received_character(ch); + fn handle_keyboard_input( + &self, + servo: &Servo, + clipboard: &mut Option, + webviews: &mut WebViewManager, + 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) { + 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 { + return; + }; + + if let Some(input_text) = &winit_event.text { + for character in input_text.chars() { + self.handle_received_character(&webview, character); } } - let mut event = keyboard_event_from_winit(&input, self.modifiers_state.get()); - trace!("handling {:?}", event); - if event.state == KeyState::Down && event.key == Key::Unidentified { + if keyboard_event.state == KeyState::Down && keyboard_event.key == Key::Unidentified { // If pressed and probably printable, we expect a ReceivedCharacter event. // Wait for that to be received and don't queue any event right now. self.last_pressed - .set(Some((event, Some(input.logical_key)))); + .set(Some((keyboard_event, Some(winit_event.logical_key)))); return; - } else if event.state == KeyState::Up && event.key == Key::Unidentified { + } else if keyboard_event.state == KeyState::Up && keyboard_event.key == Key::Unidentified { // If release and probably printable, this is following a ReceiverCharacter event. - if let Some(key) = self.keys_down.borrow_mut().remove(&input.logical_key) { - event.key = key; + if let Some(key) = self.keys_down.borrow_mut().remove(&winit_event.logical_key) { + keyboard_event.key = key; } } - if event.key != Key::Unidentified { + if keyboard_event.key != Key::Unidentified { self.last_pressed.set(None); let xr_poses = self.xr_window_poses.borrow(); for xr_window_pose in &*xr_poses { - xr_window_pose.handle_xr_rotation(&input, self.modifiers_state.get()); + xr_window_pose.handle_xr_rotation(&winit_event, self.modifiers_state.get()); } - self.event_queue - .borrow_mut() - .push(EmbedderEvent::Keyboard(event)); + webview.notify_keyboard_event(keyboard_event); } + + // servoshell also has key bindings that are visible to, and overridable by, the page. + // See the handler for EmbedderMsg::Keyboard in webview.rs for those. } /// Helper function to handle a click fn handle_mouse( &self, - button: MouseButton, - action: ElementState, + webview: &WebView, + button: winit::event::MouseButton, + action: winit::event::ElementState, coords: Point2D, ) { let max_pixel_dist = 10.0 * self.hidpi_factor().get(); @@ -256,9 +280,7 @@ impl Window { ((pixel_dist.x * pixel_dist.x + pixel_dist.y * pixel_dist.y) as f32) .sqrt(); if pixel_dist < max_pixel_dist { - self.event_queue - .borrow_mut() - .push(EmbedderEvent::MouseWindowEventClass(mouse_up_event)); + webview.notify_pointer_button_event(mouse_up_event); MouseWindowEvent::Click(mouse_button, coords.to_f32()) } else { mouse_up_event @@ -268,17 +290,138 @@ impl Window { } }, }; - self.event_queue - .borrow_mut() - .push(EmbedderEvent::MouseWindowEventClass(event)); + webview.notify_pointer_button_event(event); + } + + /// Handle key events before sending them to Servo. + fn handle_intercepted_key_bindings( + &self, + servo: &Servo, + clipboard: &mut Option, + webviews: &mut WebViewManager, + key_event: &KeyboardEvent, + ) -> bool { + let Some(focused_webview) = webviews + .focused_webview() + .map(|webview| webview.servo_webview.clone()) + else { + return false; + }; + + let mut handled = true; + 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()); + }) + .shortcut(CMD_OR_CONTROL, 'P', || { + let rate = env::var("SAMPLING_RATE") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(10); + let duration = env::var("SAMPLING_DURATION") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(10); + focused_webview.toggle_sampling_profiler( + Duration::from_millis(rate), + Duration::from_secs(duration), + ); + }) + .shortcut(CMD_OR_CONTROL, 'X', || { + focused_webview.notify_clipboard_event(ClipboardEventType::Cut); + }) + .shortcut(CMD_OR_CONTROL, 'C', || { + focused_webview.notify_clipboard_event(ClipboardEventType::Copy); + }) + .shortcut(CMD_OR_CONTROL, 'V', || { + let text = clipboard + .as_mut() + .and_then(|clipboard| clipboard.get_text().ok()) + .unwrap_or_default(); + focused_webview.notify_clipboard_event(ClipboardEventType::Paste(text)); + }) + .shortcut(Modifiers::CONTROL, Key::F9, || { + focused_webview.capture_webrender(); + }) + .shortcut(Modifiers::CONTROL, Key::F10, || { + focused_webview.toggle_webrender_debugging(WebRenderDebugOption::RenderTargetDebug); + }) + .shortcut(Modifiers::CONTROL, Key::F11, || { + focused_webview.toggle_webrender_debugging(WebRenderDebugOption::TextureCacheDebug); + }) + .shortcut(Modifiers::CONTROL, Key::F12, || { + focused_webview.toggle_webrender_debugging(WebRenderDebugOption::Profiler); + }) + .shortcut(CMD_OR_ALT, Key::ArrowRight, || { + focused_webview.go_forward(1); + }) + .optional_shortcut( + cfg!(not(target_os = "windows")), + CMD_OR_CONTROL, + ']', + || { + focused_webview.go_forward(1); + }, + ) + .shortcut(CMD_OR_ALT, Key::ArrowLeft, || { + focused_webview.go_back(1); + }) + .optional_shortcut( + cfg!(not(target_os = "windows")), + CMD_OR_CONTROL, + '[', + || { + focused_webview.go_back(1); + }, + ) + .optional_shortcut( + self.get_fullscreen(), + Modifiers::empty(), + Key::Escape, + || 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)) + // 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(); + if len > 0 { + webviews.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()) + } + }) + .shortcut(Modifiers::CONTROL, Key::PageUp, || { + if let Some(index) = webviews.get_focused_webview_index() { + let new_index = if index == 0 { + webviews.webviews().len() - 1 + } else { + index - 1 + }; + webviews.focus_webview_by_index(new_index) + } + }) + .shortcut(CMD_OR_CONTROL, 'T', || { + webviews.add(servo.new_webview(Url::parse("servo:newtab").unwrap())); + }) + .shortcut(CMD_OR_CONTROL, 'Q', || servo.start_shutting_down()) + .otherwise(|| handled = false); + handled } } impl WindowPortsMethods for Window { - fn get_events(&self) -> Vec { - std::mem::take(&mut *self.event_queue.borrow_mut()) - } - fn device_hidpi_factor(&self) -> Scale { Scale::new(self.winit_window.scale_factor() as f32) } @@ -299,7 +442,7 @@ impl WindowPortsMethods for Window { self.winit_window.set_title(title); } - fn request_inner_size(&self, size: DeviceIntSize) -> Option { + fn request_resize(&self, _: &ServoShellWebView, 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); @@ -393,25 +536,36 @@ impl WindowPortsMethods for Window { self.winit_window.id() } - fn queue_embedder_events_for_winit_event(&self, event: winit::event::WindowEvent) { + 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 { + return; + }; + match event { winit::event::WindowEvent::KeyboardInput { event, .. } => { - self.handle_keyboard_input(event) + self.handle_keyboard_input(servo, clipboard, webviews, event) }, winit::event::WindowEvent::ModifiersChanged(modifiers) => { self.modifiers_state.set(modifiers.state()) }, winit::event::WindowEvent::MouseInput { state, button, .. } => { if button == MouseButton::Left || button == MouseButton::Right { - self.handle_mouse(button, state, self.mouse_pos.get()); + self.handle_mouse(&webview, button, state, self.mouse_pos.get()); } }, winit::event::WindowEvent::CursorMoved { position, .. } => { let position = winit_position_to_euclid_point(position); self.mouse_pos.set(position.to_i32()); - self.event_queue - .borrow_mut() - .push(EmbedderEvent::MouseWindowMoveEventClass(position.to_f32())); + webview.notify_pointer_move_event(position.to_f32()); }, winit::event::WindowEvent::MouseWheel { delta, phase, .. } => { let (mut dx, mut dy, mode) = match delta { @@ -434,7 +588,6 @@ impl WindowPortsMethods for Window { }; let pos = self.mouse_pos.get(); let position = Point2D::new(pos.x as f32, pos.y as f32); - let wheel_event = EmbedderEvent::Wheel(wheel_delta, position); // Scroll events snap to the major axis of movement, with vertical // preferred over horizontal. @@ -446,47 +599,35 @@ impl WindowPortsMethods for Window { let scroll_location = ScrollLocation::Delta(Vector2D::new(dx as f32, dy as f32)); let phase = winit_phase_to_touch_event_type(phase); - let scroll_event = - EmbedderEvent::Scroll(scroll_location, self.mouse_pos.get(), phase); // Send events - self.event_queue.borrow_mut().push(wheel_event); - self.event_queue.borrow_mut().push(scroll_event); + webview.notify_wheel_event(wheel_delta, position); + webview.notify_scroll_event(scroll_location, self.mouse_pos.get(), phase); }, winit::event::WindowEvent::Touch(touch) => { - let phase = winit_phase_to_touch_event_type(touch.phase); + let touch_event_type = winit_phase_to_touch_event_type(touch.phase); let id = TouchId(touch.id as i32); let position = touch.location; let point = Point2D::new(position.x as f32, position.y as f32); - self.event_queue - .borrow_mut() - .push(EmbedderEvent::Touch(phase, id, point)); + webview.notify_touch_event(touch_event_type, id, point); }, winit::event::WindowEvent::PinchGesture { delta, .. } => { - let magnification = delta as f32 + 1.0; - self.event_queue - .borrow_mut() - .push(EmbedderEvent::PinchZoom(magnification)); + webview.set_pinch_zoom(delta as f32 + 1.0); }, winit::event::WindowEvent::CloseRequested => { - self.event_queue.borrow_mut().push(EmbedderEvent::Quit); + servo.start_shutting_down(); }, winit::event::WindowEvent::Resized(new_size) => { if self.inner_size.get() != new_size { self.inner_size.set(new_size); - self.event_queue - .borrow_mut() - .push(EmbedderEvent::WindowResize); + webview.notify_rendering_context_resized(); } }, winit::event::WindowEvent::ThemeChanged(theme) => { - let theme = match theme { + webview.notify_theme_change(match theme { winit::window::Theme::Light => Theme::Light, winit::window::Theme::Dark => Theme::Dark, - }; - self.event_queue - .borrow_mut() - .push(EmbedderEvent::ThemeChange(theme)); + }); }, _ => {}, } diff --git a/ports/servoshell/desktop/headless_window.rs b/ports/servoshell/desktop/headless_window.rs index 06dbaa5983b..9e41adfbe42 100644 --- a/ports/servoshell/desktop/headless_window.rs +++ b/ports/servoshell/desktop/headless_window.rs @@ -6,16 +6,16 @@ use std::cell::Cell; use std::rc::Rc; -use std::sync::RwLock; +use arboard::Clipboard; use euclid::num::Zero; use euclid::{Box2D, Length, Point2D, Scale, Size2D}; -use servo::compositing::windowing::{ - AnimationState, EmbedderCoordinates, EmbedderEvent, WindowMethods, -}; +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 crate::desktop::window_trait::WindowPortsMethods; pub struct Window { @@ -25,7 +25,6 @@ pub struct Window { inner_size: Cell, screen_size: Size2D, window_rect: Box2D, - event_queue: RwLock>, } impl Window { @@ -55,7 +54,6 @@ impl Window { inner_size, screen_size, window_rect, - event_queue: RwLock::new(Vec::new()), }; Rc::new(window) @@ -63,18 +61,11 @@ impl Window { } impl WindowPortsMethods for Window { - fn get_events(&self) -> Vec { - match self.event_queue.write() { - Ok(ref mut event_queue) => std::mem::take(event_queue), - Err(_) => vec![], - } - } - fn id(&self) -> winit::window::WindowId { winit::window::WindowId::dummy() } - fn request_inner_size(&self, size: DeviceIntSize) -> Option { + fn request_resize(&self, webview: &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 { @@ -82,9 +73,12 @@ impl WindowPortsMethods for Window { } self.inner_size.set(new_size); - if let Ok(ref mut queue) = self.event_queue.write() { - queue.push(EmbedderEvent::WindowResize); - } + + // 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(); + Some(new_size) } @@ -116,7 +110,13 @@ impl WindowPortsMethods for Window { self.animation_state.get() == AnimationState::Animating } - fn queue_embedder_events_for_winit_event(&self, _event: winit::event::WindowEvent) { + fn handle_winit_event( + &self, + _: &Servo, + _: &mut Option, + _: &mut WebViewManager, + _: winit::event::WindowEvent, + ) { // Not expecting any winit events. } diff --git a/ports/servoshell/desktop/minibrowser.rs b/ports/servoshell/desktop/minibrowser.rs index 0dda5b2fa3e..9a3d90288b2 100644 --- a/ports/servoshell/desktop/minibrowser.rs +++ b/ports/servoshell/desktop/minibrowser.rs @@ -20,22 +20,18 @@ use gleam::gl; use glow::NativeFramebuffer; use log::{trace, warn}; use servo::base::id::WebViewId; -use servo::compositing::windowing::EmbedderEvent; use servo::servo_geometry::DeviceIndependentPixel; use servo::servo_url::ServoUrl; use servo::webrender_api::units::DevicePixel; use servo::webrender_traits::SurfmanRenderingContext; -use servo::{TopLevelBrowsingContextId, TraversalDirection}; +use servo::Servo; use winit::event::{ElementState, MouseButton, WindowEvent}; use winit::event_loop::ActiveEventLoop; use winit::window::Window; use super::egui_glue::EguiGlow; use super::geometry::winit_position_to_euclid_point; -use super::webview::{LoadStatus, WebViewManager}; -use super::window_trait::WindowPortsMethods; -use crate::parser::location_bar_input_to_url; -use crate::prefs::ServoShellPreferences; +use super::webview::{LoadStatus, WebView, WebViewManager}; pub struct Minibrowser { pub context: EguiGlow, @@ -60,11 +56,12 @@ pub struct Minibrowser { pub enum MinibrowserEvent { /// Go button clicked. - Go, + Go(String), Back, Forward, Reload, NewWebView, + CloseWebView(WebViewId), } fn truncate_with_ellipsis(input: &str, max_length: usize) -> String { @@ -116,6 +113,10 @@ impl Minibrowser { } } + pub(crate) fn take_events(&self) -> Vec { + self.event_queue.take() + } + /// Preprocess the given [winit::event::WindowEvent], returning unconsumed for mouse events in /// the Servo browser rect. This is needed because the CentralPanel we create for our webview /// would otherwise make egui report events in that area as consumed. @@ -169,15 +170,15 @@ impl Minibrowser { .min_size(Vec2 { x: 20.0, y: 20.0 }) } - /// Draws a browser tab, checking for clicks and returns an appropriate [EmbedderEvent] + /// 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, - selected: bool, - webview_id: TopLevelBrowsingContextId, - ) -> Option { + webview: &WebView, + event_queue: &mut Vec, + ) { 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; @@ -213,6 +214,7 @@ impl Minibrowser { visuals.widgets.hovered.rounding = rounding; visuals.widgets.inactive.rounding = rounding; + let selected = webview.focused; let tab = ui.add(SelectableLabel::new( selected, truncate_with_ellipsis(label, 20), @@ -242,11 +244,9 @@ 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() { - Some(EmbedderEvent::CloseWebView(webview_id)) + event_queue.push(MinibrowserEvent::CloseWebView(webview.servo_webview.id())) } else if !selected && tab.clicked() { - Some(EmbedderEvent::FocusWebView(webview_id)) - } else { - None + webview.servo_webview.focus(); } } @@ -256,8 +256,8 @@ impl Minibrowser { pub fn update( &mut self, window: &Window, - webviews: &mut WebViewManager, - servo_framebuffer_id: Option, + webviews: &mut WebViewManager, + servo: Option<&mut Servo>, reason: &'static str, ) { let now = Instant::now(); @@ -277,6 +277,9 @@ impl Minibrowser { .. } = self; let widget_fbo = *widget_surface_fbo; + let servo_framebuffer_id = servo + .as_ref() + .and_then(|servo| 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 @@ -342,8 +345,9 @@ impl Minibrowser { if location_field.lost_focus() && ui.input(|i| i.clone().key_pressed(Key::Enter)) { - event_queue.borrow_mut().push(MinibrowserEvent::Go); - location_dirty.set(false); + event_queue + .borrow_mut() + .push(MinibrowserEvent::Go(location.borrow().clone())); } }, ); @@ -352,26 +356,19 @@ impl Minibrowser { }); }; - let mut embedder_events = vec![]; - // A simple Tab header strip TopBottomPanel::top("tabs").show(ctx, |ui| { ui.allocate_ui_with_layout( ui.available_size(), egui::Layout::left_to_right(egui::Align::Center), |ui| { - for (webview_id, webview) in webviews.webviews().into_iter() { + 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", }; - if let Some(event) = - Self::browser_tab(ui, label, webview.focused, webview_id) - { - location_dirty.set(false); - embedder_events.push(event); - } + Self::browser_tab(ui, label, webview, &mut event_queue.borrow_mut()); } if ui.add(Minibrowser::toolbar_button("+")).clicked() { event_queue.borrow_mut().push(MinibrowserEvent::NewWebView); @@ -408,8 +405,7 @@ impl Minibrowser { ) * scale; if rect != webview.rect { webview.rect = rect; - embedder_events - .push(EmbedderEvent::MoveResizeWebView(focused_webview_id, rect)); + webview.servo_webview.move_resize(rect) } let min = ui.cursor().min; let size = ui.available_size(); @@ -471,10 +467,6 @@ impl Minibrowser { }); }); - if !embedder_events.is_empty() { - webviews.handle_window_events(embedder_events); - } - *last_update = now; }); } @@ -491,58 +483,9 @@ impl Minibrowser { self.context.paint(window); } - /// Takes any outstanding events from the [Minibrowser], converting them to [EmbedderEvent] and - /// routing those to the App event queue. - pub fn queue_embedder_events_for_minibrowser_events( - &self, - browser: &WebViewManager, - app_event_queue: &mut Vec, - preferences: &ServoShellPreferences, - ) { - for event in self.event_queue.borrow_mut().drain(..) { - let browser_id = browser.focused_webview_id().unwrap(); - match event { - MinibrowserEvent::Go => { - let location = self.location.borrow(); - let Some(url) = - location_bar_input_to_url(&location.clone(), &preferences.searchpage) - else { - warn!("failed to parse location"); - break; - }; - app_event_queue.push(EmbedderEvent::LoadUrl(browser_id, url)); - }, - MinibrowserEvent::Back => { - app_event_queue.push(EmbedderEvent::Navigation( - browser_id, - TraversalDirection::Back(1), - )); - }, - MinibrowserEvent::Forward => { - app_event_queue.push(EmbedderEvent::Navigation( - browser_id, - TraversalDirection::Forward(1), - )); - }, - MinibrowserEvent::Reload => { - let browser_id = browser.focused_webview_id().unwrap(); - app_event_queue.push(EmbedderEvent::Reload(browser_id)); - }, - MinibrowserEvent::NewWebView => { - self.location_dirty.set(false); - let url = ServoUrl::parse("servo:newtab").unwrap(); - app_event_queue.push(EmbedderEvent::NewWebView(url, WebViewId::new())); - }, - } - } - } - /// 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, browser: &mut WebViewManager) -> bool { // User edited without clicking Go? if self.location_dirty.get() { return false; @@ -557,21 +500,19 @@ impl Minibrowser { } } + pub fn update_location_dirty(&self, dirty: bool) { + 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 { + 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_status_text( - &mut self, - browser: &mut WebViewManager, - ) -> bool { + 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 @@ -579,10 +520,7 @@ impl Minibrowser { /// 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, browser: &mut WebViewManager) -> 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 diff --git a/ports/servoshell/desktop/tracing.rs b/ports/servoshell/desktop/tracing.rs index fd34a9237bc..cee0752658a 100644 --- a/ports/servoshell/desktop/tracing.rs +++ b/ports/servoshell/desktop/tracing.rs @@ -36,21 +36,7 @@ macro_rules! trace_embedder_msg { }; } -/// Log an event to servo ([servo::compositing::windowing::EmbedderEvent]) at trace level. -/// - To disable tracing: RUST_LOG='servoshell>servo@=off' -/// - To enable tracing: RUST_LOG='servoshell>servo@' -/// - Recommended filters when tracing is enabled: -/// - servoshell>servo@Idle=off -/// - servoshell>servo@MouseWindowMoveEventClass=off -macro_rules! trace_embedder_event { - // This macro only exists to put the docs in the same file as the target prefix, - // so the macro definition is always the same. - ($event:expr, $($rest:tt)+) => { - ::log::trace!(target: $crate::desktop::tracing::LogTarget::log_target(&$event), $($rest)+) - }; -} - -pub(crate) use {trace_embedder_event, trace_embedder_msg, trace_winit_event}; +pub(crate) use {trace_embedder_msg, trace_winit_event}; /// Get the log target for an event, as a static string. pub(crate) trait LogTarget { @@ -185,61 +171,3 @@ mod from_servo { } } } - -mod to_servo { - use super::LogTarget; - - macro_rules! target { - ($($name:literal)+) => { - concat!("servoshell>servo@", $($name),+) - }; - } - - impl LogTarget for servo::compositing::windowing::EmbedderEvent { - fn log_target(&self) -> &'static str { - match self { - Self::Idle => target!("Idle"), - Self::Refresh => target!("Refresh"), - Self::WindowResize => target!("WindowResize"), - Self::ThemeChange(..) => target!("ThemeChange"), - Self::AllowNavigationResponse(..) => target!("AllowNavigationResponse"), - Self::LoadUrl(..) => target!("LoadUrl"), - Self::MouseWindowEventClass(..) => target!("MouseWindowEventClass"), - Self::MouseWindowMoveEventClass(..) => target!("MouseWindowMoveEventClass"), - Self::Touch(..) => target!("Touch"), - Self::Wheel(..) => target!("Wheel"), - Self::Scroll(..) => target!("Scroll"), - Self::Zoom(..) => target!("Zoom"), - Self::PinchZoom(..) => target!("PinchZoom"), - Self::ResetZoom => target!("ResetZoom"), - Self::Navigation(..) => target!("Navigation"), - Self::Quit => target!("Quit"), - Self::ExitFullScreen(..) => target!("ExitFullScreen"), - Self::Keyboard(..) => target!("Keyboard"), - Self::Reload(..) => target!("Reload"), - Self::NewWebView(..) => target!("NewWebView"), - Self::CloseWebView(..) => target!("CloseWebView"), - Self::SendError(..) => target!("SendError"), - Self::MoveResizeWebView(..) => target!("MoveResizeWebView"), - Self::ShowWebView(..) => target!("ShowWebView"), - Self::HideWebView(..) => target!("HideWebView"), - Self::RaiseWebViewToTop(..) => target!("RaiseWebViewToTop"), - Self::FocusWebView(..) => target!("FocusWebView"), - Self::BlurWebView => target!("BlurWebView"), - Self::ToggleWebRenderDebug(..) => target!("ToggleWebRenderDebug"), - Self::CaptureWebRender => target!("CaptureWebRender"), - Self::ClearCache => target!("ClearCache"), - Self::ToggleSamplingProfiler(..) => target!("ToggleSamplingProfiler"), - Self::MediaSessionAction(..) => target!("MediaSessionAction"), - Self::SetWebViewThrottled(..) => target!("SetWebViewThrottled"), - Self::IMEComposition(..) => target!("IMEComposition"), - Self::IMEDismissed => target!("IMEDismissed"), - Self::InvalidateNativeSurface => target!("InvalidateNativeSurface"), - Self::ReplaceNativeSurface(..) => target!("ReplaceNativeSurface"), - Self::Gamepad(..) => target!("Gamepad"), - Self::Vsync => target!("Vsync"), - Self::ClipboardAction(..) => target!("ClipboardAction"), - } - } - } -} diff --git a/ports/servoshell/desktop/webview.rs b/ports/servoshell/desktop/webview.rs index 1793673ec5c..e9fb15d5ea0 100644 --- a/ports/servoshell/desktop/webview.rs +++ b/ports/servoshell/desktop/webview.rs @@ -2,42 +2,37 @@ * 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::hash_map::Entry; use std::collections::HashMap; use std::fs::File; use std::io::Write; use std::rc::Rc; -use std::time::Duration; -use std::vec::Drain; use std::{env, thread}; use arboard::Clipboard; -use euclid::{Point2D, Vector2D}; +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, trace, warn}; -use servo::base::id::TopLevelBrowsingContextId as WebViewId; -use servo::compositing::windowing::{EmbedderEvent, WebRenderDebugOption}; +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::{ - ClipboardEventType, CompositorEventVariant, ContextMenuResult, DualRumbleEffectParams, - EmbedderMsg, FilterPattern, GamepadEvent, GamepadHapticEffectType, GamepadIndex, - GamepadInputBounds, GamepadSupportedHapticEffects, GamepadUpdateType, PermissionPrompt, - PermissionRequest, PromptCredentialsInput, PromptDefinition, PromptOrigin, PromptResult, - TouchEventType, TraversalDirection, + CompositorEventVariant, ContextMenuResult, DualRumbleEffectParams, EmbedderMsg, FilterPattern, + GamepadEvent, GamepadHapticEffectType, GamepadIndex, GamepadInputBounds, + GamepadSupportedHapticEffects, GamepadUpdateType, PermissionPrompt, PermissionRequest, + PromptCredentialsInput, PromptDefinition, PromptOrigin, PromptResult, Servo, TouchEventType, }; use tinyfiledialogs::{self, MessageBoxIcon, OkCancel, YesNo}; -use super::keyutils::{CMD_OR_ALT, CMD_OR_CONTROL}; +use super::keyutils::CMD_OR_CONTROL; use super::window_trait::{WindowPortsMethods, LINE_HEIGHT}; -use crate::desktop::tracing::{trace_embedder_event, trace_embedder_msg}; +use crate::desktop::tracing::trace_embedder_msg; -pub struct WebViewManager { +pub struct WebViewManager { status_text: Option, /// List of top-level browsing contexts. @@ -52,25 +47,12 @@ pub struct WebViewManager { /// Modified by EmbedderMsg::WebViewFocused and EmbedderMsg::WebViewBlurred. focused_webview_id: Option, - /// Pre-creation state for WebViews. - /// This is needed because in some situations the WebViewOpened event is sent - /// after ChangePageTitle and HistoryChanged - webview_preload_data: HashMap, - - window: Rc, - event_queue: Vec, - clipboard: Option, + window: Rc, gamepad: Option, haptic_effects: HashMap, shutdown_requested: bool, } -#[derive(Clone, Default)] -struct WebViewPreloadData { - title: Option, - url: Option, -} - #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum LoadStatus { HeadParsed, @@ -79,23 +61,24 @@ pub enum LoadStatus { } // The state of each Tab/WebView -#[derive(Debug)] pub struct WebView { pub rect: DeviceRect, pub title: Option, pub url: Option, pub focused: bool, pub load_status: LoadStatus, + pub servo_webview: ::servo::WebView, } impl WebView { - fn new(rect: DeviceRect, preload_data: WebViewPreloadData) -> Self { + fn new(servo_webview: ::servo::WebView) -> Self { Self { - rect, - title: preload_data.title, - url: preload_data.url, + rect: DeviceRect::zero(), + title: None, + url: None, focused: false, load_status: LoadStatus::LoadComplete, + servo_webview, } } } @@ -110,25 +93,14 @@ pub struct HapticEffect { pub sender: IpcSender, } -impl WebViewManager -where - Window: WindowPortsMethods + ?Sized, -{ - pub fn new(window: Rc) -> WebViewManager { +impl WebViewManager { + pub fn new(window: Rc) -> WebViewManager { WebViewManager { status_text: None, webviews: HashMap::default(), creation_order: vec![], focused_webview_id: None, - webview_preload_data: HashMap::default(), window, - clipboard: match Clipboard::new() { - Ok(c) => Some(c), - Err(e) => { - warn!("Error creating clipboard context ({})", e); - None - }, - }, gamepad: match Gilrs::new() { Ok(g) => Some(g), Err(e) => { @@ -137,8 +109,6 @@ where }, }, haptic_effects: HashMap::default(), - - event_queue: Vec::new(), shutdown_requested: false, } } @@ -147,18 +117,41 @@ where self.webviews.get_mut(&webview_id) } - // Returns the existing preload data for the given WebView, or a new one. - fn ensure_preload_data_mut(&mut self, webview_id: &WebViewId) -> &mut WebViewPreloadData { - if let Entry::Vacant(entry) = self.webview_preload_data.entry(*webview_id) { - entry.insert(WebViewPreloadData::default()); - } - self.webview_preload_data.get_mut(webview_id).unwrap() + 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()), @@ -167,10 +160,8 @@ where } pub fn focused_webview(&self) -> Option<&WebView> { - match self.focused_webview_id { - Some(id) => self.webviews.get(&id), - None => None, - } + self.focused_webview_id + .and_then(|id| self.webviews.get(&id)) } pub fn load_status(&self) -> LoadStatus { @@ -184,10 +175,6 @@ where self.status_text.clone() } - pub fn get_events(&mut self) -> Vec { - std::mem::take(&mut self.event_queue) - } - // Returns the webviews in the creation order. pub fn webviews(&self) -> Vec<(WebViewId, &WebView)> { let mut res = vec![]; @@ -197,22 +184,15 @@ where res } - pub fn handle_window_events(&mut self, events: Vec) { - for event in events { - trace_embedder_event!(event, "{event:?}"); - match event { - EmbedderEvent::Keyboard(key_event) => { - self.handle_key_from_window(key_event); - }, - event => { - self.event_queue.push(event); - }, - } - } - } - /// 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); @@ -303,7 +283,7 @@ where } if let Some(event) = gamepad_event { - self.event_queue.push(EmbedderEvent::Gamepad(event)); + webview.notify_gamepad_event(event); } } } @@ -411,242 +391,33 @@ where self.shutdown_requested } - fn focus_webview_by_index(&self, index: usize) -> Option { - Some(EmbedderEvent::FocusWebView(self.webviews().get(index)?.0)) + pub(crate) fn focus_webview_by_index(&self, index: usize) { + if let Some((_, webview)) = self.webviews().get(index) { + webview.servo_webview.focus(); + } } - fn get_focused_webview_index(&self) -> Option { + 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) } - /// Handle key events before sending them to Servo. - fn handle_key_from_window(&mut self, key_event: KeyboardEvent) { - let embedder_event = ShortcutMatcher::from_event(key_event.clone()) - .shortcut(CMD_OR_CONTROL, 'R', || { - self.focused_webview_id.map(EmbedderEvent::Reload) - }) - // Select the first 8 tabs via shortcuts - .shortcut(CMD_OR_CONTROL, '1', || self.focus_webview_by_index(0)) - .shortcut(CMD_OR_CONTROL, '2', || self.focus_webview_by_index(1)) - .shortcut(CMD_OR_CONTROL, '3', || self.focus_webview_by_index(2)) - .shortcut(CMD_OR_CONTROL, '4', || self.focus_webview_by_index(3)) - .shortcut(CMD_OR_CONTROL, '5', || self.focus_webview_by_index(4)) - .shortcut(CMD_OR_CONTROL, '6', || self.focus_webview_by_index(5)) - .shortcut(CMD_OR_CONTROL, '7', || self.focus_webview_by_index(6)) - .shortcut(CMD_OR_CONTROL, '8', || self.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 = self.webviews().len(); - if len > 0 { - self.focus_webview_by_index(len - 1) - } else { - None - } - }) - .shortcut(Modifiers::CONTROL, Key::PageDown, || { - let i = self.get_focused_webview_index()?; - self.focus_webview_by_index((i + 1) % self.webviews().len()) - }) - .shortcut(Modifiers::CONTROL, Key::PageUp, || { - let index = self.get_focused_webview_index()?; - let new_index = if index == 0 { - self.webviews().len() - 1 - } else { - index - 1 - }; - self.focus_webview_by_index(new_index) - }) - .shortcut(CMD_OR_CONTROL, 'W', || { - self.focused_webview_id.map(EmbedderEvent::CloseWebView) - }) - .shortcut(CMD_OR_CONTROL, 'T', || { - let url = ServoUrl::parse("servo:newtab").unwrap(); - Some(EmbedderEvent::NewWebView(url, WebViewId::new())) - }) - .shortcut(CMD_OR_CONTROL, 'Q', || Some(EmbedderEvent::Quit)) - .shortcut(CMD_OR_CONTROL, 'P', || { - let rate = env::var("SAMPLING_RATE") - .ok() - .and_then(|s| s.parse().ok()) - .unwrap_or(10); - let duration = env::var("SAMPLING_DURATION") - .ok() - .and_then(|s| s.parse().ok()) - .unwrap_or(10); - Some(EmbedderEvent::ToggleSamplingProfiler( - Duration::from_millis(rate), - Duration::from_secs(duration), - )) - }) - .shortcut(CMD_OR_CONTROL, 'X', || { - Some(EmbedderEvent::ClipboardAction(ClipboardEventType::Cut)) - }) - .shortcut(CMD_OR_CONTROL, 'C', || { - Some(EmbedderEvent::ClipboardAction(ClipboardEventType::Copy)) - }) - .shortcut(CMD_OR_CONTROL, 'V', || { - Some(EmbedderEvent::ClipboardAction(ClipboardEventType::Paste( - self.clipboard - .as_mut() - .and_then(|clipboard| clipboard.get_text().ok()) - .unwrap_or_else(|| { - warn!("Error getting clipboard text. Returning empty string."); - String::new() - }), - ))) - }) - .shortcut(Modifiers::CONTROL, Key::F9, || { - Some(EmbedderEvent::CaptureWebRender) - }) - .shortcut(Modifiers::CONTROL, Key::F10, || { - Some(EmbedderEvent::ToggleWebRenderDebug( - WebRenderDebugOption::RenderTargetDebug, - )) - }) - .shortcut(Modifiers::CONTROL, Key::F11, || { - Some(EmbedderEvent::ToggleWebRenderDebug( - WebRenderDebugOption::TextureCacheDebug, - )) - }) - .shortcut(Modifiers::CONTROL, Key::F12, || { - Some(EmbedderEvent::ToggleWebRenderDebug( - WebRenderDebugOption::Profiler, - )) - }) - .shortcut(CMD_OR_ALT, Key::ArrowRight, || { - self.focused_webview_id - .map(|id| EmbedderEvent::Navigation(id, TraversalDirection::Forward(1))) - }) - .optional_shortcut( - cfg!(not(target_os = "windows")), - CMD_OR_CONTROL, - ']', - || { - self.focused_webview_id() - .map(|id| EmbedderEvent::Navigation(id, TraversalDirection::Forward(1))) - }, - ) - .shortcut(CMD_OR_ALT, Key::ArrowLeft, || { - self.focused_webview_id - .map(|id| EmbedderEvent::Navigation(id, TraversalDirection::Back(1))) - }) - .optional_shortcut( - cfg!(not(target_os = "windows")), - CMD_OR_CONTROL, - '[', - || { - self.focused_webview_id - .map(|id| EmbedderEvent::Navigation(id, TraversalDirection::Back(1))) - }, - ) - .optional_shortcut( - self.window.get_fullscreen(), - Modifiers::empty(), - Key::Escape, - || self.focused_webview_id.map(EmbedderEvent::ExitFullScreen), - ) - .otherwise(|| { - self.focused_webview_id - .map(|_id| EmbedderEvent::Keyboard(key_event)) - }) - .flatten(); - if let Some(event) = embedder_event { - self.event_queue.push(event); - } - } - - /// Handle key events after they have been handled by Servo. - fn handle_key_from_servo(&mut self, webview_id: Option, event: KeyboardEvent) { - ShortcutMatcher::from_event(event) - .shortcut(CMD_OR_CONTROL, '=', || { - self.event_queue.push(EmbedderEvent::Zoom(1.1)) - }) - .shortcut(CMD_OR_CONTROL, '+', || { - self.event_queue.push(EmbedderEvent::Zoom(1.1)) - }) - .shortcut(CMD_OR_CONTROL, '-', || { - self.event_queue.push(EmbedderEvent::Zoom(1.0 / 1.1)) - }) - .shortcut(CMD_OR_CONTROL, '0', || { - self.event_queue.push(EmbedderEvent::ResetZoom) - }) - .shortcut(Modifiers::empty(), Key::PageDown, || { - let scroll_location = ScrollLocation::Delta(Vector2D::new( - 0.0, - -self.window.page_height() + 2.0 * LINE_HEIGHT, - )); - self.scroll_window_from_key(scroll_location, TouchEventType::Move, webview_id); - }) - .shortcut(Modifiers::empty(), Key::PageUp, || { - let scroll_location = ScrollLocation::Delta(Vector2D::new( - 0.0, - self.window.page_height() - 2.0 * LINE_HEIGHT, - )); - self.scroll_window_from_key(scroll_location, TouchEventType::Move, webview_id); - }) - .shortcut(Modifiers::empty(), Key::Home, || { - self.scroll_window_from_key( - ScrollLocation::Start, - TouchEventType::Move, - webview_id, - ); - }) - .shortcut(Modifiers::empty(), Key::End, || { - self.scroll_window_from_key(ScrollLocation::End, TouchEventType::Move, webview_id); - }) - .shortcut(Modifiers::empty(), Key::ArrowUp, || { - self.scroll_window_from_key( - ScrollLocation::Delta(Vector2D::new(0.0, 3.0 * LINE_HEIGHT)), - TouchEventType::Move, - webview_id, - ); - }) - .shortcut(Modifiers::empty(), Key::ArrowDown, || { - self.scroll_window_from_key( - ScrollLocation::Delta(Vector2D::new(0.0, -3.0 * LINE_HEIGHT)), - TouchEventType::Move, - webview_id, - ); - }) - .shortcut(Modifiers::empty(), Key::ArrowLeft, || { - self.scroll_window_from_key( - ScrollLocation::Delta(Vector2D::new(LINE_HEIGHT, 0.0)), - TouchEventType::Move, - webview_id, - ); - }) - .shortcut(Modifiers::empty(), Key::ArrowRight, || { - self.scroll_window_from_key( - ScrollLocation::Delta(Vector2D::new(-LINE_HEIGHT, 0.0)), - TouchEventType::Move, - webview_id, - ); - }); - } - - fn scroll_window_from_key( - &mut self, - scroll_location: ScrollLocation, - phase: TouchEventType, - webview_id: Option, - ) { - // In minibrowser mode the webview is offset by the toolbar - let origin = webview_id - .and_then(|id| self.webviews.get(&id)) - .map(|webview| webview.rect.min.ceil().to_i32()) - .unwrap_or(Point2D::zero()); - let event = EmbedderEvent::Scroll(scroll_location, origin, phase); - self.event_queue.push(event); + fn send_error(&self, webview_id: Option, error: String) { + let Some(webview) = webview_id.and_then(|id| self.get(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, - events: Drain<'_, (Option, EmbedderMsg)>, + events: Vec<(Option, EmbedderMsg)>, ) -> ServoEventResponse { let mut need_present = self.load_status() != LoadStatus::LoadComplete; let mut need_update = false; @@ -656,6 +427,7 @@ where } else { trace_embedder_msg!(msg, "{msg:?}"); } + match msg { EmbedderMsg::Status(status) => { self.status_text = status; @@ -674,31 +446,24 @@ where )); } need_update = true; - } else { - let data = self.ensure_preload_data_mut(&webview_id); - data.title = title.clone(); } } }, EmbedderMsg::MoveTo(point) => { self.window.set_position(point); }, - EmbedderMsg::ResizeTo(size) => { + EmbedderMsg::ResizeTo(inner_size) => { if let Some(webview_id) = webview_id { - let new_rect = self.get_mut(webview_id).and_then(|webview| { - if webview.rect.size() != size.to_f32() { - webview.rect.set_size(size.to_f32()); - Some(webview.rect) - } else { - None + 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(new_rect) = new_rect { - self.event_queue - .push(EmbedderEvent::MoveResizeWebView(webview_id, new_rect)); + }; + if let Some(webview) = self.get(webview_id) { + self.window.request_resize(webview, inner_size); } } - self.window.request_inner_size(size); }, EmbedderMsg::Prompt(definition, origin) => { let res = if opts.headless { @@ -785,33 +550,32 @@ where .expect("Thread spawning failed") }; if let Err(e) = res { - let reason = format!("Failed to send Prompt response: {}", e); - self.event_queue - .push(EmbedderEvent::SendError(webview_id, reason)); + self.send_error(webview_id, format!("Failed to send Prompt response: {e}")) } }, EmbedderMsg::AllowUnload(sender) => { // Always allow unload for now. if let Err(e) = sender.send(true) { - let reason = format!("Failed to send AllowUnload response: {}", e); - self.event_queue - .push(EmbedderEvent::SendError(webview_id, reason)); + self.send_error( + webview_id, + format!("Failed to send AllowUnload response: {e}"), + ) } }, EmbedderMsg::AllowNavigationRequest(pipeline_id, _url) => { if let Some(_webview_id) = webview_id { - self.event_queue - .push(EmbedderEvent::AllowNavigationResponse(pipeline_id, true)); + servo.allow_navigation_response(pipeline_id, true); } }, EmbedderMsg::AllowOpeningWebView(response_chan) => { - // Note: would be a place to handle pop-ups config. - // see Step 7 of #the-rules-for-choosing-a-browsing-context-given-a-browsing-context-name - if let Err(e) = response_chan.send(Some(WebViewId::new())) { - warn!("Failed to send AllowOpeningWebView response: {}", e); - }; + 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) => { + println!("WebView opened: {:?}", new_webview_id); let scale = self.window.hidpi_factor().get(); let toolbar = self.window.toolbar_height().get(); @@ -820,41 +584,30 @@ where let mut rect = self.window.get_coordinates().get_viewport().to_f32(); rect.min.y += toolbar * scale; - // Make sure to not add duplicates into the creation_order vector. - // This can happen as explained in https://github.com/servo/servo/issues/33075 - let preload_data = self.ensure_preload_data_mut(&new_webview_id).clone(); - if let Entry::Vacant(entry) = self.webviews.entry(new_webview_id) { - entry.insert(WebView::new(rect, preload_data)); - self.creation_order.push(new_webview_id); - self.event_queue - .push(EmbedderEvent::FocusWebView(new_webview_id)); - self.event_queue - .push(EmbedderEvent::MoveResizeWebView(new_webview_id, rect)); - self.event_queue - .push(EmbedderEvent::RaiseWebViewToTop(new_webview_id, true)); - } + 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.webviews.retain(|&id, _| id != webview_id); - self.creation_order.retain(|&id| id != webview_id); - self.focused_webview_id = None; - if let Some(&newest_webview_id) = self.creation_order.last() { - self.event_queue - .push(EmbedderEvent::FocusWebView(newest_webview_id)); - } else { - self.event_queue.push(EmbedderEvent::Quit); - } + self.close_webview(servo, webview_id); }, EmbedderMsg::WebViewFocused(webview_id) => { - for (id, webview) in &mut self.webviews { - webview.focused = *id == webview_id; + if let Some(webview) = self.focused_webview_id.and_then(|id| self.get_mut(id)) { + webview.focused = false; } - self.focused_webview_id = Some(webview_id); - need_update = true; + // Show the most recently created webview and hide all others. // TODO: Stop doing this once we have full multiple webviews support - self.event_queue - .push(EmbedderEvent::ShowWebView(webview_id, true)); + 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() { @@ -863,28 +616,24 @@ where self.focused_webview_id = None; }, EmbedderMsg::Keyboard(key_event) => { - self.handle_key_from_servo(webview_id, key_event); + self.handle_overridable_key_bindings(webview_id, key_event); }, EmbedderMsg::ClearClipboardContents => { - self.clipboard + clipboard .as_mut() .and_then(|clipboard| clipboard.clear().ok()); }, EmbedderMsg::GetClipboardContents(sender) => { - let contents = self - .clipboard + let contents = clipboard .as_mut() .and_then(|clipboard| clipboard.get_text().ok()) - .unwrap_or_else(|| { - warn!("Error getting clipboard text. Returning empty string."); - String::new() - }); + .unwrap_or_default(); if let Err(e) = sender.send(contents) { warn!("Failed to send clipboard ({})", e); } }, EmbedderMsg::SetClipboardContents(text) => { - if let Some(ref mut clipboard) = self.clipboard { + if let Some(clipboard) = clipboard.as_mut() { if let Err(e) = clipboard.set_text(text) { warn!("Error setting clipboard contents ({})", e); } @@ -897,42 +646,31 @@ where // FIXME: show favicons in the UI somehow }, EmbedderMsg::HeadParsed => { - if let Some(webview_id) = webview_id { - if let Some(webview) = self.get_mut(webview_id) { - webview.load_status = LoadStatus::HeadParsed; - need_update = true; - } - } + if let Some(webview) = webview_id.and_then(|id| self.get_mut(id)) { + webview.load_status = LoadStatus::HeadParsed; + need_update = true; + }; }, EmbedderMsg::HistoryChanged(urls, current) => { - if let Some(webview_id) = webview_id { - if let Some(webview) = self.get_mut(webview_id) { - webview.url = Some(urls[current].clone()); - need_update = true; - } else { - let data = self.ensure_preload_data_mut(&webview_id); - data.url = Some(urls[current].clone()); - } - } + if let Some(webview) = webview_id.and_then(|id| self.get_mut(id)) { + webview.url = Some(urls[current].clone()); + need_update = true; + }; }, EmbedderMsg::SetFullscreenState(state) => { self.window.set_fullscreen(state); }, EmbedderMsg::LoadStart => { - if let Some(webview_id) = webview_id { - if let Some(webview) = self.get_mut(webview_id) { - webview.load_status = LoadStatus::LoadStart; - need_update = true; - } - } + if let Some(webview) = webview_id.and_then(|id| self.get_mut(id)) { + webview.load_status = LoadStatus::LoadStart; + need_update = true; + }; }, EmbedderMsg::LoadComplete => { - if let Some(webview_id) = webview_id { - if let Some(webview) = self.get_mut(webview_id) { - webview.load_status = LoadStatus::LoadComplete; - need_update = true; - } - } + if let Some(webview) = webview_id.and_then(|id| self.get_mut(id)) { + webview.load_status = LoadStatus::LoadComplete; + need_update = true; + }; }, EmbedderMsg::WebResourceRequested(_web_resource_request, _response_sender) => {}, EmbedderMsg::Shutdown => { @@ -942,21 +680,23 @@ where EmbedderMsg::GetSelectedBluetoothDevice(devices, sender) => { let selected = platform_get_selected_devices(devices); if let Err(e) = sender.send(selected) { - let reason = - format!("Failed to send GetSelectedBluetoothDevice response: {}", e); - self.event_queue - .push(EmbedderEvent::SendError(None, reason)); + self.send_error( + webview_id, + format!("Failed to send GetSelectedBluetoothDevice response: {e}"), + ); }; }, EmbedderMsg::SelectFiles(patterns, multiple_files, sender) => { - let res = match (opts.headless, get_selected_files(patterns, multiple_files)) { + let result = match (opts.headless, get_selected_files(patterns, multiple_files)) + { (true, _) | (false, None) => sender.send(None), (false, Some(files)) => sender.send(Some(files)), }; - if let Err(e) = res { - let reason = format!("Failed to send SelectFiles response: {}", e); - self.event_queue - .push(EmbedderEvent::SendError(None, reason)); + if let Err(e) = result { + self.send_error( + webview_id, + format!("Failed to send SelectFiles response: {e}"), + ); }; }, EmbedderMsg::PromptPermission(prompt, sender) => { @@ -993,15 +733,12 @@ where need_present = true; }, EmbedderMsg::EventDelivered(event) => { - if let (Some(webview_id), CompositorEventVariant::MouseButtonEvent) = - (webview_id, event) - { - trace!("{}: Got a mouse button event", webview_id); - self.event_queue - .push(EmbedderEvent::RaiseWebViewToTop(webview_id, true)); - self.event_queue - .push(EmbedderEvent::FocusWebView(webview_id)); - } + if let Some(webview) = webview_id.and_then(|id| self.get_mut(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 { @@ -1024,6 +761,69 @@ where 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: Option, + event: KeyboardEvent, + ) { + let Some(webview) = webview_id.and_then(|id| self.get(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")] diff --git a/ports/servoshell/desktop/window_trait.rs b/ports/servoshell/desktop/window_trait.rs index 33d00f1b1bf..91338ac7aed 100644 --- a/ports/servoshell/desktop/window_trait.rs +++ b/ports/servoshell/desktop/window_trait.rs @@ -7,17 +7,19 @@ use std::rc::Rc; +use arboard::Clipboard; use euclid::{Length, Scale}; -use servo::compositing::windowing::{EmbedderEvent, WindowMethods}; +use servo::compositing::windowing::WindowMethods; use servo::servo_geometry::DeviceIndependentPixel; use servo::webrender_api::units::{DeviceIntPoint, DeviceIntSize, DevicePixel}; -use servo::Cursor; +use servo::{Cursor, Servo}; + +use super::webview::{WebView, WebViewManager}; // This should vary by zoom level and maybe actual text size (focused or under cursor) pub const LINE_HEIGHT: f32 = 38.0; pub trait WindowPortsMethods: WindowMethods { - fn get_events(&self) -> Vec; fn id(&self) -> winit::window::WindowId; fn hidpi_factor(&self) -> Scale { self.device_pixel_ratio_override() @@ -29,10 +31,18 @@ pub trait WindowPortsMethods: WindowMethods { ) -> Option>; fn page_height(&self) -> f32; fn get_fullscreen(&self) -> bool; - fn queue_embedder_events_for_winit_event(&self, event: winit::event::WindowEvent); + fn handle_winit_event( + &self, + servo: &Servo, + clipboard: &mut Option, + webviews: &mut WebViewManager, + event: winit::event::WindowEvent, + ); fn is_animating(&self) -> bool; fn set_title(&self, _title: &str) {} - fn request_inner_size(&self, size: DeviceIntSize) -> Option; + /// Request a new inner size for the window, not including external decorations. + fn request_resize(&self, webview: &WebView, inner_size: DeviceIntSize) + -> Option; fn set_position(&self, _point: DeviceIntPoint) {} fn set_fullscreen(&self, _state: bool) {} fn set_cursor(&self, _cursor: Cursor) {}