/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use std::cell::{Cell, RefCell}; use std::rc::Rc; use euclid::Scale; use log::warn; use servo::{ AuthenticationRequest, BluetoothDeviceSelectionRequest, ConsoleLogLevel, Cursor, DeviceIndependentIntRect, DeviceIndependentPixel, DeviceIntPoint, DeviceIntSize, DevicePixel, EmbedderControl, EmbedderControlId, InputEventId, InputEventResult, MediaSessionEvent, PermissionRequest, RenderingContext, ScreenGeometry, WebView, WebViewBuilder, WebViewId, }; use url::Url; use crate::parser::location_bar_input_to_url; use crate::running_app_state::{RunningAppState, UserInterfaceCommand, WebViewCollection}; // This should vary by zoom level and maybe actual text size (focused or under cursor) #[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))] pub(crate) const LINE_HEIGHT: f32 = 76.0; #[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))] pub(crate) const LINE_WIDTH: f32 = 76.0; /// /// "A window size of 10x10px shouldn't be supported by any browser." #[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))] pub(crate) const MIN_WINDOW_INNER_SIZE: DeviceIntSize = DeviceIntSize::new(100, 100); #[derive(Copy, Clone, Eq, Hash, PartialEq)] pub(crate) struct ServoShellWindowId(u64); impl From for ServoShellWindowId { fn from(value: u64) -> Self { Self(value) } } pub(crate) struct ServoShellWindow { /// The [`WebView`]s that have been added to this window. pub(crate) webview_collection: RefCell, /// A handle to the [`PlatformWindow`] that servoshell is rendering in. platform_window: Rc, /// Whether or not this window should be closed at the end of the spin of the next event loop. close_scheduled: Cell, /// Whether or not the application interface needs to be updated. needs_update: Cell, /// Whether or not Servo needs to repaint its display. Currently this is global /// because every `WebView` shares a `RenderingContext`. needs_repaint: Cell, /// List of webviews that have favicon textures which are not yet uploaded /// to the GPU by egui. pending_favicon_loads: RefCell>, /// Pending [`UserInterfaceCommand`] that have yet to be processed by the main loop. pending_commands: RefCell>, } impl ServoShellWindow { pub(crate) fn new(platform_window: Rc) -> Self { Self { webview_collection: Default::default(), platform_window, close_scheduled: Default::default(), needs_update: Default::default(), needs_repaint: Default::default(), pending_favicon_loads: Default::default(), pending_commands: Default::default(), } } pub(crate) fn id(&self) -> ServoShellWindowId { self.platform_window().id() } /// Must be called *after* `self` is in `state.windows`, otherwise it will panic. pub(crate) fn create_and_activate_toplevel_webview( &self, state: Rc, url: Url, ) -> WebView { let webview = self.create_toplevel_webview(state, url); self.activate_webview(webview.id()); webview } /// Must be called *after* `self` is in `state.windows`, otherwise it will panic. #[servo::servo_tracing::instrument(skip(self, state))] pub(crate) fn create_toplevel_webview(&self, state: Rc, url: Url) -> WebView { let mut webview_builder = WebViewBuilder::new(state.servo(), self.platform_window.rendering_context()) .url(url) .hidpi_scale_factor(self.platform_window.hidpi_scale_factor()) .user_content_manager(state.user_content_manager.clone()) .delegate(state.clone()); #[cfg(all( feature = "gamepad", not(any(target_os = "android", target_env = "ohos")) ))] if let Some(gamepad_delegate) = state.gamepad_delegate() { webview_builder = webview_builder.gamepad_delegate(gamepad_delegate); } let webview = webview_builder.build(); webview.notify_theme_change(self.platform_window.theme()); self.add_webview(webview.clone()); // If `self` is not in `state.windows`, our notify_accessibility_tree_update() will panic. if state.accessibility_active() { // Activate accessibility in the WebView. // There are two sites like this; this is the WebView creation site. webview.set_accessibility_active(true); } webview } /// Repaint the focused [`WebView`]. pub(crate) fn repaint_webviews(&self) { let Some(webview) = self.active_webview() else { return; }; self.platform_window() .rendering_context() .make_current() .expect("Could not make PlatformWindow RenderingContext current"); webview.paint(); self.platform_window().rendering_context().present(); } /// Whether or not this [`ServoShellWindow`] has any [`WebView`]s. pub(crate) fn should_close(&self) -> bool { self.webview_collection.borrow().is_empty() || self.close_scheduled.get() } pub(crate) fn contains_webview(&self, id: WebViewId) -> bool { self.webview_collection.borrow().contains(id) } pub(crate) fn webview_by_id(&self, id: WebViewId) -> Option { self.webview_collection.borrow().get(id).cloned() } pub(crate) fn set_needs_update(&self) { self.needs_update.set(true); } pub(crate) fn set_needs_repaint(&self) { self.needs_repaint.set(true) } #[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))] pub(crate) fn schedule_close(&self) { self.close_scheduled.set(true) } pub(crate) fn platform_window(&self) -> Rc { self.platform_window.clone() } pub(crate) fn focus(&self) { self.platform_window.focus() } pub(crate) fn add_webview(&self, webview: WebView) { self.webview_collection.borrow_mut().add(webview); self.set_needs_update(); self.set_needs_repaint(); } pub(crate) fn webview_ids(&self) -> Vec { self.webview_collection.borrow().creation_order.clone() } /// Returns all [`WebView`]s in creation order. pub(crate) fn webviews(&self) -> Vec<(WebViewId, WebView)> { self.webview_collection .borrow() .all_in_creation_order() .map(|(id, webview)| (id, webview.clone())) .collect() } pub(crate) fn activate_webview(&self, webview_id: WebViewId) { self.webview_collection .borrow_mut() .activate_webview(webview_id); self.set_needs_update(); } #[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))] pub(crate) fn activate_webview_by_index(&self, index_to_activate: usize) { self.webview_collection .borrow_mut() .activate_webview_by_index(index_to_activate); self.set_needs_update(); } #[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))] pub(crate) fn get_active_webview_index(&self) -> Option { let active_id = self.webview_collection.borrow().active_id()?; self.webviews() .iter() .position(|webview| webview.0 == active_id) } pub(crate) fn update_and_request_repaint_if_necessary(&self, state: &RunningAppState) { let updated_user_interface = self.needs_update.take() && self.platform_window .update_user_interface_state(state, self); // Delegate handlers may have asked us to present or update painted WebView contents. // Currently, egui-file-dialog dialogs need to be constantly redrawn or animations aren't fluid. let needs_repaint = self.needs_repaint.take(); if updated_user_interface || needs_repaint { self.platform_window.request_repaint(self); } } /// Close the given [`WebView`] via its [`WebViewId`]. /// /// Note: This can happen because we can trigger a close with a UI action and then get /// the close notification via the [`WebViewDelegate`] later. pub(crate) fn close_webview(&self, webview_id: WebViewId) { let mut webview_collection = self.webview_collection.borrow_mut(); if webview_collection.remove(webview_id).is_none() { return; } self.platform_window .dismiss_embedder_controls_for_webview(webview_id); self.set_needs_update(); self.set_needs_repaint(); } pub(crate) fn notify_favicon_changed(&self, webview: WebView) { self.pending_favicon_loads.borrow_mut().push(webview.id()); self.set_needs_repaint(); } #[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))] pub(crate) fn hidpi_scale_factor_changed(&self) { let new_scale_factor = self.platform_window.hidpi_scale_factor(); for webview in self.webview_collection.borrow().values() { webview.set_hidpi_scale_factor(new_scale_factor); } } pub(crate) fn active_webview(&self) -> Option { self.webview_collection.borrow().active().cloned() } #[cfg_attr( not(any(target_os = "android", target_env = "ohos")), expect(dead_code) )] pub(crate) fn active_or_newest_webview(&self) -> Option { let webview_collection = self.webview_collection.borrow(); webview_collection .active() .or(webview_collection.newest()) .cloned() } /// Return a list of all webviews that have favicons that have not yet been loaded by egui. #[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))] pub(crate) fn take_pending_favicon_loads(&self) -> Vec { std::mem::take(&mut *self.pending_favicon_loads.borrow_mut()) } pub(crate) fn show_embedder_control( &self, webview: WebView, embedder_control: EmbedderControl, ) { self.platform_window .show_embedder_control(webview.id(), embedder_control); self.set_needs_update(); self.set_needs_repaint(); } pub(crate) fn hide_embedder_control( &self, webview: WebView, embedder_control: EmbedderControlId, ) { self.platform_window .hide_embedder_control(webview.id(), embedder_control); self.set_needs_update(); self.set_needs_repaint(); } pub(crate) fn queue_user_interface_command(&self, command: UserInterfaceCommand) { self.pending_commands.borrow_mut().push(command) } /// Takes any events generated during UI updates and performs their actions. pub(crate) fn handle_interface_commands( &self, state: &Rc, create_platform_window: Option<&dyn Fn(Url) -> Rc>, ) { let commands = std::mem::take(&mut *self.pending_commands.borrow_mut()); for event in commands { match event { UserInterfaceCommand::Go(location) => { self.set_needs_update(); let Some(url) = location_bar_input_to_url( &location.clone(), &state.servoshell_preferences.searchpage, ) else { warn!("failed to parse location"); break; }; if let Some(active_webview) = self.active_webview() { active_webview.load(url.into_url()); } }, UserInterfaceCommand::Back => { if let Some(active_webview) = self.active_webview() { active_webview.go_back(1); } }, UserInterfaceCommand::Forward => { if let Some(active_webview) = self.active_webview() { active_webview.go_forward(1); } }, UserInterfaceCommand::Reload => { self.set_needs_update(); if let Some(active_webview) = self.active_webview() { active_webview.reload(); } }, UserInterfaceCommand::ReloadAll => { for window in state.windows().values() { window.set_needs_update(); for (_, webview) in window.webviews() { webview.reload(); } } }, UserInterfaceCommand::NewWebView => { self.set_needs_update(); let url = Url::parse("servo:newtab").expect("Should always be able to parse"); self.create_and_activate_toplevel_webview(state.clone(), url); }, UserInterfaceCommand::CloseWebView(id) => { self.set_needs_update(); self.close_webview(id); }, UserInterfaceCommand::NewWindow => { if let Some(create_platform_window) = create_platform_window { let url = Url::parse("servo:newtab").unwrap(); let platform_window = create_platform_window(url.clone()); state.open_window(platform_window, url); } }, } } } } /// A `PlatformWindow` abstracts away the differents kinds of platform windows that might /// be used in a servoshell execution. This currently includes headed (winit) and headless /// windows. pub(crate) trait PlatformWindow { fn id(&self) -> ServoShellWindowId; fn screen_geometry(&self) -> ScreenGeometry; #[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))] fn device_hidpi_scale_factor(&self) -> Scale; fn hidpi_scale_factor(&self) -> Scale; #[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))] fn get_fullscreen(&self) -> bool; /// Request that the `Window` rebuild its user interface, if it has one. This should /// not repaint, but should prepare the user interface for painting when it is /// actually requested. #[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))] fn rebuild_user_interface(&self, _: &RunningAppState, _: &ServoShellWindow) {} /// Inform the `Window` that the state of a `WebView` has changed and that it should /// do an incremental update of user interface state. Returns `true` if the user /// interface actually changed and a rebuild and repaint is needed, `false` otherwise. fn update_user_interface_state(&self, _: &RunningAppState, _: &ServoShellWindow) -> bool { false } /// Request that the window redraw itself. It is up to the window to do this /// once the windowing system is ready. If this is a headless window, the redraw /// will happen immediately. fn request_repaint(&self, _: &ServoShellWindow); /// Request a new outer size for the window, including external decorations. /// This should be the same as `window.outerWidth` and `window.outerHeight`` fn request_resize(&self, webview: &WebView, outer_size: DeviceIntSize) -> Option; fn set_position(&self, _point: DeviceIntPoint) {} fn set_fullscreen(&self, _state: bool) {} fn set_cursor(&self, _cursor: Cursor) {} #[cfg(all( feature = "webxr", not(any(target_os = "android", target_env = "ohos")) ))] fn new_glwindow( &self, event_loop: &winit::event_loop::ActiveEventLoop, ) -> Rc; /// This returns [`RenderingContext`] matching the viewport. fn rendering_context(&self) -> Rc; fn theme(&self) -> servo::Theme { servo::Theme::Light } fn window_rect(&self) -> DeviceIndependentIntRect; fn maximize(&self, _: &WebView) {} fn focus(&self) {} fn has_platform_focus(&self) -> bool { true } fn show_embedder_control(&self, _: WebViewId, _: EmbedderControl) {} fn hide_embedder_control(&self, _: WebViewId, _: EmbedderControlId) {} fn dismiss_embedder_controls_for_webview(&self, _: WebViewId) {} fn show_bluetooth_device_dialog( &self, _: WebViewId, _request: BluetoothDeviceSelectionRequest, ) { } fn show_permission_dialog(&self, _: WebViewId, _: PermissionRequest) {} fn show_http_authentication_dialog(&self, _: WebViewId, _: AuthenticationRequest) {} fn notify_input_event_handled( &self, _webview: &WebView, _id: InputEventId, _result: InputEventResult, ) { } fn notify_media_session_event(&self, _: MediaSessionEvent) {} fn notify_crashed(&self, _: WebView, _reason: String, _backtrace: Option) {} fn show_console_message(&self, _level: ConsoleLogLevel, _message: &str) {} #[cfg(not(any(target_os = "android", target_env = "ohos")))] /// If this window is a headed window, access the concrete type. fn as_headed_window(&self) -> Option<&crate::desktop::headed_window::HeadedWindow> { None } #[cfg(any(target_os = "android", target_env = "ohos"))] /// If this window is a headed window, access the concrete type. fn as_headed_window(&self) -> Option<&crate::egl::app::EmbeddedPlatformWindow> { None } fn notify_accessibility_tree_update(&self, _: WebView, _: accesskit::TreeUpdate) {} }