diff --git a/components/shared/embedder/webdriver.rs b/components/shared/embedder/webdriver.rs index 96a853bd882..6123ac6b7f1 100644 --- a/components/shared/embedder/webdriver.rs +++ b/components/shared/embedder/webdriver.rs @@ -138,7 +138,7 @@ pub enum WebDriverCommandMsg { HandleUserPrompt( WebViewId, WebDriverUserPromptAction, - IpcSender, ()>>, + IpcSender>, ), GetAlertText(WebViewId, IpcSender>), SendAlertText(WebViewId, String), diff --git a/components/webdriver_server/user_prompt.rs b/components/webdriver_server/user_prompt.rs index d5b42a5ea17..633f1b2bf9a 100644 --- a/components/webdriver_server/user_prompt.rs +++ b/components/webdriver_server/user_prompt.rs @@ -333,9 +333,7 @@ impl Handler { if handler.notify || handler.handler == WebDriverUserPromptAction::Ignore { // Step 6. If handler's notify is true, return annotated unexpected alert open error. - let alert_text = wait_for_ipc_response(receiver)? - .unwrap_or_default() - .unwrap_or_default(); + let alert_text = wait_for_ipc_response(receiver)?.unwrap_or_default(); Err(WebDriverError::new_with_data( ErrorStatus::UnexpectedAlertOpen, diff --git a/ports/servoshell/desktop/app.rs b/ports/servoshell/desktop/app.rs index a132baecd54..d67a18a4638 100644 --- a/ports/servoshell/desktop/app.rs +++ b/ports/servoshell/desktop/app.rs @@ -4,64 +4,48 @@ //! Application entry point, runs the event loop. -use std::collections::HashMap; use std::path::Path; use std::rc::Rc; use std::time::Instant; use std::{env, fs}; -use ::servo::ServoBuilder; -use crossbeam_channel::unbounded; -use log::warn; use net::protocols::ProtocolRegistry; use servo::config::opts::Opts; use servo::config::prefs::Preferences; use servo::servo_url::ServoUrl; use servo::user_content_manager::{UserContentManager, UserScript}; -use servo::{EventLoopWaker, WebDriverCommandMsg, WebDriverUserPromptAction}; -use url::Url; +use servo::{EventLoopWaker, ServoBuilder}; use winit::application::ApplicationHandler; use winit::event::WindowEvent; use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoopProxy}; use winit::window::WindowId; -use super::app_state::AppState; use super::event_loop::AppEvent; use super::{headed_window, headless_window}; -use crate::desktop::app_state::RunningAppState; use crate::desktop::event_loop::ServoShellEventLoop; use crate::desktop::protocols; use crate::desktop::tracing::trace_winit_event; -use crate::desktop::window_trait::WindowPortsMethods; use crate::parser::get_default_url; use crate::prefs::ServoShellPreferences; -use crate::running_app_state::RunningAppStateTrait; +use crate::running_app_state::RunningAppState; +use crate::window::PlatformWindow; + +pub(crate) enum AppState { + Initializing, + Running(Rc), + ShuttingDown, +} pub struct App { opts: Opts, preferences: Preferences, servoshell_preferences: ServoShellPreferences, waker: Box, - proxy: Option>, + event_loop_proxy: Option>, initial_url: ServoUrl, t_start: Instant, t: Instant, state: AppState, - - // This is the last field of the struct to ensure that windows are dropped *after* all other - // references to the relevant rendering contexts have been destroyed. - // (https://github.com/servo/servo/issues/36711) - windows: HashMap>, -} - -/// Action to be taken by the caller of [`App::handle_events`]. -pub(crate) enum PumpResult { - /// The caller should shut down Servo and its related context. - Shutdown, - Continue { - needs_user_interface_update: bool, - need_window_redraw: bool, - }, } impl App { @@ -83,9 +67,8 @@ impl App { opts, preferences, servoshell_preferences: servo_shell_preferences, - windows: HashMap::new(), waker: event_loop.create_event_loop_waker(), - proxy: event_loop.event_loop_proxy(), + event_loop_proxy: event_loop.event_loop_proxy(), initial_url: initial_url.clone(), t_start: t, t, @@ -94,27 +77,7 @@ impl App { } /// Initialize Application once event loop start running. - pub fn init(&mut self, event_loop: Option<&ActiveEventLoop>) { - let headless = self.servoshell_preferences.headless; - assert_eq!(headless, event_loop.is_none()); - - let window = match event_loop { - Some(event_loop) => { - let event_loop_proxy = self.proxy.take().expect("Must have a proxy available"); - Rc::new(headed_window::Window::new( - &self.servoshell_preferences, - event_loop, - event_loop_proxy, - self.initial_url.clone(), - )) - }, - None => headless_window::Window::new(&self.servoshell_preferences), - }; - - self.windows.insert(window.id(), window); - - let (_, window) = self.windows.iter().next().unwrap(); - + pub fn init(&mut self, active_event_loop: Option<&ActiveEventLoop>) { let mut user_content_manager = UserContentManager::new(); for script in load_userscripts(self.servoshell_preferences.userscripts_directory.as_deref()) .expect("Loading userscripts failed") @@ -141,278 +104,63 @@ impl App { .protocol_registry(protocol_registry) .event_loop_waker(self.waker.clone()); + let platform_window = + self.create_platform_window(self.initial_url.clone(), active_event_loop); #[cfg(feature = "webxr")] let servo_builder = servo_builder.webxr_registry(super::webxr::XrDiscoveryWebXrRegistry::new_boxed( - window.clone(), - event_loop, + platform_window.clone(), + active_event_loop, &self.preferences, )); let servo = servo_builder.build(); servo.setup_logging(); - // Initialize WebDriver server here before `servo` is moved. - let webdriver_receiver = self.servoshell_preferences.webdriver_port.map(|port| { - let (embedder_sender, embedder_receiver) = unbounded(); - webdriver_server::start_server(port, embedder_sender, self.waker.clone()); - embedder_receiver - }); - let running_state = Rc::new(RunningAppState::new( servo, - window.clone(), self.servoshell_preferences.clone(), - webdriver_receiver, + self.waker.clone(), )); - - running_state.create_and_focus_toplevel_webview(self.initial_url.clone().into_url()); - window.rebuild_user_interface(&running_state); + running_state.create_window(platform_window, self.initial_url.as_url().clone()); self.state = AppState::Running(running_state); } + fn create_platform_window( + &self, + url: ServoUrl, + active_event_loop: Option<&ActiveEventLoop>, + ) -> Rc { + assert_eq!( + self.servoshell_preferences.headless, + active_event_loop.is_none() + ); + + let Some(active_event_loop) = active_event_loop else { + return headless_window::Window::new(&self.servoshell_preferences); + }; + + headed_window::Window::new( + &self.servoshell_preferences, + active_event_loop, + self.event_loop_proxy + .clone() + .expect("Should always have event loop proxy in headed mode."), + url, + ) + } + pub fn pump_servo_event_loop(&mut self) -> bool { let AppState::Running(state) = &self.state else { return false; }; - self.handle_webdriver_messages(); - match state.pump_event_loop() { - PumpResult::Shutdown => { - state.webview_collection_mut().clear(); - self.state = AppState::ShuttingDown; - false - }, - PumpResult::Continue { - needs_user_interface_update, - need_window_redraw, - } => { - // TODO: These results should eventually be stored per-`Window`. - for window in self.windows.values() { - let updated_user_interface = - needs_user_interface_update && window.update_user_interface_state(state); - if updated_user_interface || need_window_redraw { - window.request_repaint(state); - } - } - true - }, - } - } - - pub(crate) fn handle_webdriver_messages(&self) { - let AppState::Running(running_state) = &self.state else { - return; - }; - - let Some(webdriver_receiver) = running_state.webdriver_receiver() else { - return; - }; - - while let Ok(msg) = webdriver_receiver.try_recv() { - match msg { - WebDriverCommandMsg::Shutdown => { - running_state.servo().start_shutting_down(); - }, - WebDriverCommandMsg::IsWebViewOpen(webview_id, sender) => { - let context = running_state.webview_by_id(webview_id); - - if let Err(error) = sender.send(context.is_some()) { - warn!("Failed to send response of IsWebViewOpen: {error}"); - } - }, - WebDriverCommandMsg::IsBrowsingContextOpen(..) => { - running_state.servo().execute_webdriver_command(msg); - }, - WebDriverCommandMsg::NewWebView(response_sender, load_status_sender) => { - let new_webview = - running_state.create_toplevel_webview(Url::parse("about:blank").unwrap()); - - if let Err(error) = response_sender.send(new_webview.id()) { - warn!("Failed to send response of NewWebview: {error}"); - } - if let Some(load_status_sender) = load_status_sender { - running_state.set_load_status_sender(new_webview.id(), load_status_sender); - } - }, - WebDriverCommandMsg::CloseWebView(webview_id, response_sender) => { - running_state.close_webview(webview_id); - if let Err(error) = response_sender.send(()) { - warn!("Failed to send response of CloseWebView: {error}"); - } - }, - WebDriverCommandMsg::FocusWebView(webview_id) => { - if let Some(webview) = running_state.webview_by_id(webview_id) { - webview.focus_and_raise_to_top(true); - } - }, - WebDriverCommandMsg::FocusBrowsingContext(..) => { - running_state.servo().execute_webdriver_command(msg); - }, - WebDriverCommandMsg::GetAllWebViews(response_sender) => { - let webviews = running_state.webviews().iter().map(|(id, _)| *id).collect(); - - if let Err(error) = response_sender.send(webviews) { - warn!("Failed to send response of GetAllWebViews: {error}"); - } - }, - WebDriverCommandMsg::GetWindowRect(_webview_id, response_sender) => { - let window = self - .windows - .values() - .next() - .expect("Should have at least one window in servoshell"); - - if let Err(error) = response_sender.send(window.window_rect()) { - warn!("Failed to send response of GetWindowSize: {error}"); - } - }, - WebDriverCommandMsg::MaximizeWebView(webview_id, response_sender) => { - let window = self - .windows - .values() - .next() - .expect("Should have at least one window in servoshell"); - window.maximize( - &running_state - .webview_by_id(webview_id) - .expect("Webview must exists as we just verified"), - ); - - if let Err(error) = response_sender.send(window.window_rect()) { - warn!("Failed to send response of GetWindowSize: {error}"); - } - }, - WebDriverCommandMsg::SetWindowRect(webview_id, requested_rect, size_sender) => { - let Some(webview) = running_state.webview_by_id(webview_id) else { - continue; - }; - - let window = self - .windows - .values() - .next() - .expect("Should have at least one window in servoshell"); - let scale = window.hidpi_scale_factor(); - - let requested_physical_rect = - (requested_rect.to_f32() * scale).round().to_i32(); - - // Step 17. Set Width/Height. - window.request_resize(&webview, requested_physical_rect.size()); - - // Step 18. Set position of the window. - window.set_position(requested_physical_rect.min); - - if let Err(error) = size_sender.send(window.window_rect()) { - warn!("Failed to send window size: {error}"); - } - }, - WebDriverCommandMsg::GetViewportSize(_webview_id, response_sender) => { - let window = self - .windows - .values() - .next() - .expect("Should have at least one window in servoshell"); - - let size = window.rendering_context().size2d(); - - if let Err(error) = response_sender.send(size) { - warn!("Failed to send response of GetViewportSize: {error}"); - } - }, - // This is only received when start new session. - WebDriverCommandMsg::GetFocusedWebView(sender) => { - let focused_webview = running_state.focused_webview(); - - if let Err(error) = sender.send(focused_webview.map(|w| w.id())) { - warn!("Failed to send response of GetFocusedWebView: {error}"); - }; - }, - WebDriverCommandMsg::LoadUrl(webview_id, url, load_status_sender) => { - running_state.handle_webdriver_load_url(webview_id, url, load_status_sender); - }, - WebDriverCommandMsg::Refresh(webview_id, load_status_sender) => { - if let Some(webview) = running_state.webview_by_id(webview_id) { - running_state.set_load_status_sender(webview_id, load_status_sender); - webview.reload(); - } - }, - WebDriverCommandMsg::GoBack(webview_id, load_status_sender) => { - if let Some(webview) = running_state.webview_by_id(webview_id) { - let traversal_id = webview.go_back(1); - running_state.set_pending_traversal(traversal_id, load_status_sender); - } - }, - WebDriverCommandMsg::GoForward(webview_id, load_status_sender) => { - if let Some(webview) = running_state.webview_by_id(webview_id) { - let traversal_id = webview.go_forward(1); - running_state.set_pending_traversal(traversal_id, load_status_sender); - } - }, - WebDriverCommandMsg::InputEvent(webview_id, input_event, response_sender) => { - running_state.handle_webdriver_input_event( - webview_id, - input_event, - response_sender, - ); - }, - WebDriverCommandMsg::ScriptCommand(_, ref webdriver_script_command) => { - running_state.handle_webdriver_script_command(webdriver_script_command); - running_state.servo().execute_webdriver_command(msg); - }, - WebDriverCommandMsg::CurrentUserPrompt(webview_id, response_sender) => { - let current_dialog = - running_state.get_current_active_dialog_webdriver_type(webview_id); - if let Err(error) = response_sender.send(current_dialog) { - warn!("Failed to send response of CurrentUserPrompt: {error}"); - }; - }, - WebDriverCommandMsg::HandleUserPrompt(webview_id, action, response_sender) => { - let response = if running_state.webview_has_active_dialog(webview_id) { - let alert_text = running_state.alert_text_of_newest_dialog(webview_id); - - match action { - WebDriverUserPromptAction::Accept => { - running_state.accept_active_dialogs(webview_id) - }, - WebDriverUserPromptAction::Dismiss => { - running_state.dismiss_active_dialogs(webview_id) - }, - WebDriverUserPromptAction::Ignore => {}, - }; - - // Return success for AcceptAlert and DismissAlert commands. - Ok(alert_text) - } else { - // Return error for AcceptAlert and DismissAlert commands - // if there is no active dialog. - Err(()) - }; - - if let Err(error) = response_sender.send(response) { - warn!("Failed to send response of HandleUserPrompt: {error}"); - }; - }, - WebDriverCommandMsg::GetAlertText(webview_id, response_sender) => { - let response = match running_state.alert_text_of_newest_dialog(webview_id) { - Some(text) => Ok(text), - None => Err(()), - }; - - if let Err(error) = response_sender.send(response) { - warn!("Failed to send response of GetAlertText: {error}"); - }; - }, - WebDriverCommandMsg::SendAlertText(webview_id, text) => { - running_state.set_alert_text_of_newest_dialog(webview_id, text); - }, - WebDriverCommandMsg::TakeScreenshot(webview_id, rect, result_sender) => { - running_state.handle_webdriver_screenshot(webview_id, rect, result_sender); - }, - }; + if !state.spin_event_loop() { + self.state = AppState::ShuttingDown; + return false; } + true } } @@ -440,10 +188,14 @@ impl ApplicationHandler for App { let AppState::Running(state) = &self.state else { return; }; - let Some(window) = self.windows.get(&window_id) else { - return; - }; - window.handle_winit_window_event(state.clone(), window_event); + let window_id: u64 = window_id.into(); + if let Some(window) = state.window(window_id.into()) { + window.platform_window().handle_winit_window_event( + state.clone(), + &window, + window_event, + ); + } } if !self.pump_servo_event_loop() { @@ -458,10 +210,16 @@ impl ApplicationHandler for App { let AppState::Running(state) = &self.state else { return; }; - let Some(window) = self.windows.values().next() else { - return; - }; - window.handle_winit_app_event(state.clone(), app_event); + if let Some(window_id) = app_event.window_id() { + let window_id: u64 = window_id.into(); + if let Some(window) = state.window(window_id.into()) { + window.platform_window().handle_winit_app_event( + state.clone(), + &window, + app_event, + ); + } + } } if !self.pump_servo_event_loop() { diff --git a/ports/servoshell/desktop/app_state.rs b/ports/servoshell/desktop/app_state.rs deleted file mode 100644 index b8c6fffcbc3..00000000000 --- a/ports/servoshell/desktop/app_state.rs +++ /dev/null @@ -1,716 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ - -use std::cell::{Ref, RefCell, RefMut}; -use std::collections::HashMap; -use std::collections::hash_map::Entry; -use std::mem; -use std::rc::Rc; - -use crossbeam_channel::Receiver; -use log::{error, info}; -use servo::base::generic_channel::GenericSender; -use servo::base::id::WebViewId; -use servo::config::pref; -use servo::ipc_channel::ipc::IpcSender; -use servo::webrender_api::units::{DeviceIntPoint, DeviceIntSize}; -use servo::{ - AllowOrDenyRequest, AuthenticationRequest, Cursor, EmbedderControl, EmbedderControlId, - GamepadHapticEffectType, InputEventId, InputEventResult, JSValue, LoadStatus, - PermissionRequest, Servo, ServoDelegate, ServoError, SimpleDialog, TraversalId, - WebDriverCommandMsg, WebDriverLoadStatus, WebDriverUserPrompt, WebView, WebViewBuilder, - WebViewDelegate, -}; -use url::Url; - -use super::app::PumpResult; -use super::dialog::Dialog; -use super::gamepad::GamepadSupport; -use super::window_trait::WindowPortsMethods; -use crate::prefs::ServoShellPreferences; -use crate::running_app_state::{RunningAppStateBase, RunningAppStateTrait}; - -pub(crate) enum AppState { - Initializing, - Running(Rc), - ShuttingDown, -} - -pub(crate) struct RunningAppState { - base: RunningAppStateBase, - inner: RefCell, -} - -pub struct RunningAppStateInner { - /// The current set of open dialogs. - dialogs: HashMap>, - - /// A handle to the Window that Servo is rendering in -- either headed or headless. - window: Rc, - - /// Gamepad support, which may be `None` if it failed to initialize. - gamepad_support: Option, - - /// Whether or not the application interface needs to be updated. - need_update: bool, - - /// Whether or not Servo needs to repaint its display. Currently this is global - /// because every `WebView` shares a `RenderingContext`. - need_repaint: bool, - - /// Whether or not the amount of dialogs on the currently rendered webview - /// has just changed. - dialog_amount_changed: bool, - - /// List of webviews that have favicon textures which are not yet uploaded - /// to the GPU by egui. - pending_favicon_loads: Vec, - - /// A list of showing [`InputMethod`] interfaces. - visible_input_methods: Vec, -} - -impl Drop for RunningAppState { - fn drop(&mut self) { - self.servo().deinit(); - } -} - -impl RunningAppStateTrait for RunningAppState { - fn base(&self) -> &RunningAppStateBase { - &self.base - } - - fn base_mut(&mut self) -> &mut RunningAppStateBase { - &mut self.base - } - - fn webview_by_id(&self, id: WebViewId) -> Option { - self.webview_collection().get(id).cloned() - } - - fn dismiss_embedder_controls_for_webview(&self, webview_id: WebViewId) { - let _ = self.inner_mut().dialogs.remove(&webview_id); - } -} - -impl RunningAppState { - pub fn new( - servo: Servo, - window: Rc, - servoshell_preferences: ServoShellPreferences, - webdriver_receiver: Option>, - ) -> RunningAppState { - servo.set_delegate(Rc::new(ServoShellServoDelegate)); - let gamepad_support = if pref!(dom_gamepad_enabled) { - GamepadSupport::maybe_new() - } else { - None - }; - RunningAppState { - base: RunningAppStateBase::new(servoshell_preferences, servo, webdriver_receiver), - inner: RefCell::new(RunningAppStateInner { - dialogs: Default::default(), - window, - gamepad_support, - need_update: false, - need_repaint: false, - dialog_amount_changed: false, - pending_favicon_loads: Default::default(), - visible_input_methods: Default::default(), - }), - } - } - - pub(crate) fn create_and_focus_toplevel_webview(self: &Rc, url: Url) { - let webview = self.create_toplevel_webview(url); - webview.focus_and_raise_to_top(true); - } - - pub(crate) fn create_toplevel_webview(self: &Rc, url: Url) -> WebView { - let webview = WebViewBuilder::new(self.servo(), self.inner().window.rendering_context()) - .url(url) - .hidpi_scale_factor(self.inner().window.hidpi_scale_factor()) - .delegate(self.clone()) - .build(); - - webview.notify_theme_change(self.inner().window.theme()); - self.add_webview(webview.clone()); - webview - } - - pub(crate) fn inner(&self) -> Ref<'_, RunningAppStateInner> { - self.inner.borrow() - } - - pub(crate) fn inner_mut(&self) -> RefMut<'_, RunningAppStateInner> { - self.inner.borrow_mut() - } - - pub(crate) fn hidpi_scale_factor_changed(&self) { - let new_scale_factor = self.inner().window.hidpi_scale_factor(); - for webview in self.webview_collection().values() { - webview.set_hidpi_scale_factor(new_scale_factor); - } - } - - /// Repaint the Servo view is necessary, returning true if anything was actually - /// painted or false otherwise. Something may not be painted if Servo is waiting - /// for a stable image to paint. - pub(crate) fn repaint_servo_if_necessary(&self) { - if !self.inner().need_repaint { - return; - } - let Some(webview) = self.focused_webview() else { - return; - }; - - webview.paint(); - - let mut inner_mut = self.inner_mut(); - inner_mut.window.rendering_context().present(); - inner_mut.need_repaint = false; - } - - /// Spins the internal application event loop. - /// - /// - Notifies Servo about incoming gamepad events - /// - Spin the Servo event loop, which will run the compositor and trigger delegate methods. - pub(crate) fn pump_event_loop(&self) -> PumpResult { - if pref!(dom_gamepad_enabled) { - self.handle_gamepad_events(); - } - - if !self.servo().spin_event_loop() { - return PumpResult::Shutdown; - } - - // Delegate handlers may have asked us to present or update compositor contents. - // Currently, egui-file-dialog dialogs need to be constantly redrawn or animations aren't fluid. - let need_window_redraw = self.inner().need_repaint || - self.has_active_dialog() || - self.inner().dialog_amount_changed; - let need_update = std::mem::replace(&mut self.inner_mut().need_update, false); - - self.inner_mut().dialog_amount_changed = false; - - if self.servoshell_preferences().exit_after_stable_image && - self.base().achieved_stable_image.get() - { - self.servo().start_shutting_down(); - } - - PumpResult::Continue { - needs_user_interface_update: need_update, - need_window_redraw, - } - } - - pub(crate) fn for_each_active_dialog(&self, callback: impl Fn(&mut Dialog) -> bool) { - let last_created_webview_id = self - .webview_collection() - .newest() - .map(|webview_view| webview_view.id()); - let Some(webview_id) = self - .focused_webview() - .as_ref() - .map(WebView::id) - .or(last_created_webview_id) - else { - return; - }; - - let mut inner = self.inner_mut(); - - // If a dialog is open, clear any Servo cursor. TODO: This should restore the - // cursor too, when all dialogs close. In general, we need a better cursor - // management strategy. - if inner - .dialogs - .get(&webview_id) - .is_some_and(|dialogs| !dialogs.is_empty()) - { - inner.window.set_cursor(Cursor::Default); - } - - if let Some(dialogs) = inner.dialogs.get_mut(&webview_id) { - let length = dialogs.len(); - dialogs.retain_mut(callback); - if length != dialogs.len() { - inner.dialog_amount_changed = true; - } - } - } - - pub fn close_webview(&self, webview_id: WebViewId) { - // This can happen because we can trigger a close with a UI action and then get the - // close event from Servo later. - if !self.webview_collection().contains(webview_id) { - return; - } - - self.inner_mut().dialogs.remove(&webview_id); - - self.webview_collection_mut().remove(webview_id); - - match self.webview_collection().newest() { - Some(newest_webview) => { - newest_webview.focus(); - }, - None if self.servoshell_preferences().webdriver_port.is_none() => { - self.servo().start_shutting_down() - }, - None => { - // For WebDriver, don't shut down when last webview closed - // https://github.com/servo/servo/issues/37408 - }, - } - } - - pub fn handle_gamepad_events(&self) { - let Some(active_webview) = self.focused_webview() else { - return; - }; - if let Some(gamepad_support) = self.inner_mut().gamepad_support.as_mut() { - gamepad_support.handle_gamepad_events(active_webview); - } - } - - pub(crate) fn focus_webview_by_index(&self, index: usize) { - if let Some((_, webview)) = self.webviews().get(index) { - webview.focus(); - } - } - - fn add_dialog(&self, webview: servo::WebView, dialog: Dialog) { - let mut inner_mut = self.inner_mut(); - inner_mut - .dialogs - .entry(webview.id()) - .or_default() - .push(dialog); - inner_mut.need_update = true; - } - - pub(crate) fn has_active_dialog(&self) -> bool { - let last_created_webview_id = self - .webview_collection() - .newest() - .map(|webview_view| webview_view.id()); - let Some(webview_id) = self - .focused_webview() - .as_ref() - .map(WebView::id) - .or(last_created_webview_id) - else { - return false; - }; - - let inner = self.inner(); - inner - .dialogs - .get(&webview_id) - .is_some_and(|dialogs| !dialogs.is_empty()) - } - - pub(crate) fn webview_has_active_dialog(&self, webview_id: WebViewId) -> bool { - self.inner() - .dialogs - .get(&webview_id) - .is_some_and(|dialogs| !dialogs.is_empty()) - } - - pub(crate) fn get_current_active_dialog_webdriver_type( - &self, - webview_id: WebViewId, - ) -> Option { - let inner = self.inner(); - let dialogs = inner.dialogs.get(&webview_id)?; - dialogs - .iter() - .rev() - .filter_map(|dialog| dialog.webdriver_dialog_type()) - .nth(0) - } - - pub(crate) fn accept_active_dialogs(&self, webview_id: WebViewId) { - if let Some(dialogs) = self.inner_mut().dialogs.get_mut(&webview_id) { - dialogs.drain(..).for_each(|dialog| { - dialog.accept(); - }); - } - } - - pub(crate) fn dismiss_active_dialogs(&self, webview_id: WebViewId) { - if let Some(dialogs) = self.inner_mut().dialogs.get_mut(&webview_id) { - dialogs.drain(..).for_each(|dialog| { - dialog.dismiss(); - }); - } - } - - pub(crate) fn alert_text_of_newest_dialog(&self, webview_id: WebViewId) -> Option { - self.inner() - .dialogs - .get(&webview_id) - .and_then(|dialogs| dialogs.last()) - .and_then(|dialog| dialog.message()) - } - - pub(crate) fn set_alert_text_of_newest_dialog(&self, webview_id: WebViewId, text: String) { - if let Some(dialogs) = self.inner_mut().dialogs.get_mut(&webview_id) { - if let Some(dialog) = dialogs.last_mut() { - dialog.set_message(text); - } - } - } - - fn show_simple_dialog(&self, webview: servo::WebView, dialog: SimpleDialog) { - self.interrupt_webdriver_script_evaluation(); - - // Dialogs block the page load, so need need to notify WebDriver - let webview_id = webview.id(); - if let Some(sender) = self - .base() - .webdriver_senders - .borrow_mut() - .load_status_senders - .get(&webview_id) - { - let _ = sender.send(WebDriverLoadStatus::Blocked); - }; - - if self.servoshell_preferences().headless && - self.servoshell_preferences().webdriver_port.is_none() - { - // TODO: Avoid copying this from the default trait impl? - // Return the DOM-specified default value for when we **cannot show simple dialogs**. - let _ = match dialog { - SimpleDialog::Alert { - response_sender, .. - } => response_sender.send(Default::default()), - SimpleDialog::Confirm { - response_sender, .. - } => response_sender.send(Default::default()), - SimpleDialog::Prompt { - response_sender, .. - } => response_sender.send(Default::default()), - }; - return; - } - let dialog = Dialog::new_simple_dialog(dialog); - self.add_dialog(webview, dialog); - } - - pub(crate) fn get_focused_webview_index(&self) -> Option { - let focused_id = self.webview_collection().focused_id()?; - self.webviews() - .iter() - .position(|webview| webview.0 == focused_id) - } - - /// Interrupt any ongoing WebDriver-based script evaluation. - /// - /// From : - /// > The rules to execute a function body are as follows. The algorithm returns - /// > an ECMAScript completion record. - /// > - /// > If at any point during the algorithm a user prompt appears, immediately return - /// > Completion { Type: normal, Value: null, Target: empty }, but continue to run the - /// > other steps of this algorithm in parallel. - fn interrupt_webdriver_script_evaluation(&self) { - if let Some(sender) = &self - .base() - .webdriver_senders - .borrow() - .script_evaluation_interrupt_sender - { - sender.send(Ok(JSValue::Null)).unwrap_or_else(|err| { - info!( - "Notify dialog appear failed. Maybe the channel to webdriver is closed: {err}" - ); - }); - } - } - - /// Return a list of all webviews that have favicons that have not yet been loaded by egui. - pub(crate) fn take_pending_favicon_loads(&self) -> Vec { - mem::take(&mut self.inner_mut().pending_favicon_loads) - } -} - -struct ServoShellServoDelegate; -impl ServoDelegate for ServoShellServoDelegate { - fn notify_devtools_server_started(&self, _servo: &Servo, port: u16, _token: String) { - info!("Devtools Server running on port {port}"); - } - - fn request_devtools_connection(&self, _servo: &Servo, request: AllowOrDenyRequest) { - request.allow(); - } - - fn notify_error(&self, _servo: &Servo, error: ServoError) { - error!("Saw Servo error: {error:?}!"); - } -} - -impl WebViewDelegate for RunningAppState { - fn screen_geometry(&self, _webview: WebView) -> Option { - Some(self.inner().window.screen_geometry()) - } - - fn notify_status_text_changed(&self, _webview: servo::WebView, _status: Option) { - self.inner_mut().need_update = true; - } - - fn notify_history_changed(&self, _webview: WebView, _entries: Vec, _current: usize) { - self.inner_mut().need_update = true; - } - - fn notify_page_title_changed(&self, webview: servo::WebView, _: Option) { - if webview.focused() { - self.inner_mut().need_update = true; - } - } - - fn notify_traversal_complete(&self, _webview: servo::WebView, traversal_id: TraversalId) { - let mut webdriver_state = self.base().webdriver_senders.borrow_mut(); - if let Entry::Occupied(entry) = webdriver_state.pending_traversals.entry(traversal_id) { - let sender = entry.remove(); - let _ = sender.send(WebDriverLoadStatus::Complete); - } - } - - fn request_move_to(&self, _: servo::WebView, new_position: DeviceIntPoint) { - self.inner().window.set_position(new_position); - } - - fn request_resize_to(&self, webview: servo::WebView, requested_outer_size: DeviceIntSize) { - // We need to update compositor's view later as we not sure about resizing result. - self.inner() - .window - .request_resize(&webview, requested_outer_size); - } - - fn request_authentication( - &self, - webview: WebView, - authentication_request: AuthenticationRequest, - ) { - if self.servoshell_preferences().headless && - self.servoshell_preferences().webdriver_port.is_none() - { - return; - } - - self.add_dialog( - webview, - Dialog::new_authentication_dialog(authentication_request), - ); - } - - fn request_open_auxiliary_webview( - &self, - parent_webview: servo::WebView, - ) -> Option { - let webview = - WebViewBuilder::new_auxiliary(self.servo(), self.inner().window.rendering_context()) - .hidpi_scale_factor(self.inner().window.hidpi_scale_factor()) - .delegate(parent_webview.delegate()) - .build(); - - webview.notify_theme_change(self.inner().window.theme()); - // When WebDriver is enabled, do not focus and raise the WebView to the top, - // as that is what the specification expects. Otherwise, we would like `window.open()` - // to create a new foreground tab - if self.servoshell_preferences().webdriver_port.is_none() { - webview.focus_and_raise_to_top(true); - } - self.add_webview(webview.clone()); - Some(webview) - } - - fn notify_closed(&self, webview: servo::WebView) { - self.close_webview(webview.id()); - } - - fn notify_focus_changed(&self, webview: servo::WebView, focused: bool) { - if focused { - webview.show(true); - self.inner_mut().need_update = true; - self.webview_collection_mut() - .set_focused(Some(webview.id())); - } else if self.webview_collection().focused_id() == Some(webview.id()) { - self.webview_collection_mut().set_focused(None); - } - } - - fn notify_input_event_handled( - &self, - webview: WebView, - id: InputEventId, - result: InputEventResult, - ) { - self.inner() - .window - .notify_input_event_handled(&webview, id, result); - - if let Some(response_sender) = self - .base() - .pending_webdriver_events - .borrow_mut() - .remove(&id) - { - let _ = response_sender.send(()); - } - } - - fn notify_cursor_changed(&self, _webview: servo::WebView, cursor: servo::Cursor) { - self.inner().window.set_cursor(cursor); - } - - fn notify_load_status_changed(&self, webview: servo::WebView, status: LoadStatus) { - self.inner_mut().need_update = true; - - if status == LoadStatus::Complete { - if let Some(sender) = self - .base() - .webdriver_senders - .borrow_mut() - .load_status_senders - .remove(&webview.id()) - { - let _ = sender.send(WebDriverLoadStatus::Complete); - } - self.maybe_request_screenshot(webview); - } - } - - fn notify_fullscreen_state_changed(&self, _webview: servo::WebView, fullscreen_state: bool) { - self.inner().window.set_fullscreen(fullscreen_state); - } - - fn show_bluetooth_device_dialog( - &self, - webview: servo::WebView, - devices: Vec, - response_sender: GenericSender>, - ) { - self.add_dialog( - webview, - Dialog::new_device_selection_dialog(devices, response_sender), - ); - } - - fn request_permission(&self, webview: servo::WebView, permission_request: PermissionRequest) { - if self.servoshell_preferences().headless && - self.servoshell_preferences().webdriver_port.is_none() - { - permission_request.deny(); - return; - } - - let permission_dialog = Dialog::new_permission_request_dialog(permission_request); - self.add_dialog(webview, permission_dialog); - } - - fn notify_new_frame_ready(&self, _webview: servo::WebView) { - self.inner_mut().need_repaint = true; - } - - fn play_gamepad_haptic_effect( - &self, - _webview: servo::WebView, - index: usize, - effect_type: GamepadHapticEffectType, - effect_complete_sender: IpcSender, - ) { - match self.inner_mut().gamepad_support.as_mut() { - Some(gamepad_support) => { - gamepad_support.play_haptic_effect(index, effect_type, effect_complete_sender); - }, - None => { - let _ = effect_complete_sender.send(false); - }, - } - } - - fn stop_gamepad_haptic_effect( - &self, - _webview: servo::WebView, - index: usize, - haptic_stop_sender: IpcSender, - ) { - let stopped = match self.inner_mut().gamepad_support.as_mut() { - Some(gamepad_support) => gamepad_support.stop_haptic_effect(index), - None => false, - }; - let _ = haptic_stop_sender.send(stopped); - } - - fn show_embedder_control(&self, webview: WebView, embedder_control: EmbedderControl) { - if self.servoshell_preferences().headless && - self.servoshell_preferences().webdriver_port.is_none() - { - return; - } - - let control_id = embedder_control.id(); - match embedder_control { - EmbedderControl::SelectElement(prompt) => { - // FIXME: Reading the toolbar height is needed here to properly position the select dialog. - // But if the toolbar height changes while the dialog is open then the position won't be updated - let offset = self.inner().window.toolbar_height(); - self.add_dialog(webview, Dialog::new_select_element_dialog(prompt, offset)); - }, - EmbedderControl::ColorPicker(color_picker) => { - // FIXME: Reading the toolbar height is needed here to properly position the select dialog. - // But if the toolbar height changes while the dialog is open then the position won't be updated - let offset = self.inner().window.toolbar_height(); - self.add_dialog( - webview, - Dialog::new_color_picker_dialog(color_picker, offset), - ); - }, - EmbedderControl::InputMethod(input_method_control) => { - self.inner_mut().visible_input_methods.push(control_id); - self.inner().window.show_ime(input_method_control); - }, - EmbedderControl::FilePicker(file_picker) => { - self.add_dialog(webview, Dialog::new_file_dialog(file_picker)); - }, - EmbedderControl::SimpleDialog(simple_dialog) => { - self.show_simple_dialog(webview, simple_dialog); - }, - EmbedderControl::ContextMenu(prompt) => { - let offset = self.inner().window.toolbar_height(); - self.add_dialog(webview, Dialog::new_context_menu(prompt, offset)); - }, - } - } - - fn hide_embedder_control(&self, webview: WebView, control_id: EmbedderControlId) { - { - let mut inner_mut = self.inner_mut(); - if let Some(index) = inner_mut - .visible_input_methods - .iter() - .position(|visible_id| *visible_id == control_id) - { - inner_mut.visible_input_methods.remove(index); - inner_mut.window.hide_ime(); - } - } - - if let Some(dialogs) = self.inner_mut().dialogs.get_mut(&webview.id()) { - dialogs.retain(|dialog| dialog.embedder_control_id() != Some(control_id)); - } - } - - fn notify_favicon_changed(&self, webview: WebView) { - let mut inner = self.inner_mut(); - inner.pending_favicon_loads.push(webview.id()); - inner.need_repaint = true; - } -} diff --git a/ports/servoshell/desktop/dialog.rs b/ports/servoshell/desktop/dialog.rs index 72095eb7c34..7e843ce1ecd 100644 --- a/ports/servoshell/desktop/dialog.rs +++ b/ports/servoshell/desktop/dialog.rs @@ -17,7 +17,6 @@ use servo::{ AlertResponse, AuthenticationRequest, ColorPicker, ConfirmResponse, ContextMenu, ContextMenuItem, EmbedderControlId, FilePicker, PermissionRequest, PromptResponse, RgbColor, SelectElement, SelectElementOption, SelectElementOptionOrOptgroup, SimpleDialog, - WebDriverUserPrompt, }; /// The minimum width of many UI elements including dialog boxes and menus, @@ -146,40 +145,6 @@ impl Dialog { } } - pub fn accept(&self) { - #[allow(clippy::single_match)] - match self { - Dialog::SimpleDialog(dialog) => { - dialog.accept(); - }, - _ => {}, - } - } - - pub fn dismiss(&self) { - #[allow(clippy::single_match)] - match self { - Dialog::SimpleDialog(dialog) => { - dialog.dismiss(); - }, - _ => {}, - } - } - - pub fn message(&self) -> Option { - #[allow(clippy::single_match)] - match self { - Dialog::SimpleDialog(dialog) => Some(dialog.message().to_string()), - _ => None, - } - } - - pub fn set_message(&mut self, text: String) { - if let Dialog::SimpleDialog(dialog) = self { - dialog.set_message(text); - } - } - /// Returns false if the dialog has been closed, or true otherwise. pub fn update(&mut self, ctx: &egui::Context) -> bool { match self { @@ -712,25 +677,6 @@ impl Dialog { } } - pub fn webdriver_dialog_type(&self) -> Option { - // From - // > Step 3: If the current user prompt is an alert dialog, set type to "alert". Otherwise, - // > if the current user prompt is a beforeunload dialog, set type to - // > "beforeUnload". Otherwise, if the current user prompt is a confirm dialog, set - // > type to "confirm". Otherwise, if the current user prompt is a prompt dialog, - // > set type to "prompt". - match self { - Dialog::SimpleDialog(SimpleDialog::Alert { .. }) => Some(WebDriverUserPrompt::Alert), - Dialog::SimpleDialog(SimpleDialog::Confirm { .. }) => { - Some(WebDriverUserPrompt::Confirm) - }, - Dialog::SimpleDialog(SimpleDialog::Prompt { .. }) => Some(WebDriverUserPrompt::Prompt), - Dialog::File { .. } => Some(WebDriverUserPrompt::File), - Dialog::SelectElement { .. } => Some(WebDriverUserPrompt::Default), - _ => None, - } - } - pub(crate) fn embedder_control_id(&self) -> Option { match self { Dialog::SelectElement { maybe_prompt, .. } => { diff --git a/ports/servoshell/desktop/event_loop.rs b/ports/servoshell/desktop/event_loop.rs index 26d9133c7dc..eb45f07fe89 100644 --- a/ports/servoshell/desktop/event_loop.rs +++ b/ports/servoshell/desktop/event_loop.rs @@ -10,6 +10,7 @@ use std::time; use log::warn; use servo::EventLoopWaker; use winit::event_loop::{EventLoop, EventLoop as WinitEventLoop, EventLoopProxy}; +use winit::window::WindowId; use super::app::App; @@ -26,6 +27,15 @@ impl From for AppEvent { } } +impl AppEvent { + pub(crate) fn window_id(&self) -> Option { + match self { + AppEvent::Waker => None, + AppEvent::Accessibility(event) => Some(event.window_id), + } + } +} + /// A headed or headless event loop. Headless event loops are necessary for environments without a /// display server. Ideally, we could use the headed winit event loop in both modes, but on Linux, /// the event loop requires a display server, which prevents running servoshell in a console. diff --git a/ports/servoshell/desktop/gui.rs b/ports/servoshell/desktop/gui.rs index bcf58eb5ecf..18f3d239f24 100644 --- a/ports/servoshell/desktop/gui.rs +++ b/ports/servoshell/desktop/gui.rs @@ -25,11 +25,12 @@ use winit::event::{ElementState, MouseButton, WindowEvent}; use winit::event_loop::{ActiveEventLoop, EventLoopProxy}; use winit::window::Window; -use super::app_state::RunningAppState; use super::geometry::winit_position_to_euclid_point; use crate::desktop::event_loop::AppEvent; +use crate::desktop::headed_window; use crate::prefs::{EXPERIMENTAL_PREFS, ServoShellPreferences}; -use crate::running_app_state::RunningAppStateTrait; +use crate::running_app_state::RunningAppState; +use crate::window::ServoShellWindow; /// The user interface of a headed servoshell. Currently this is implemented via /// egui. @@ -94,7 +95,7 @@ impl Drop for Gui { } impl Gui { - pub fn new( + pub(crate) fn new( winit_window: &Window, event_loop: &ActiveEventLoop, event_loop_proxy: EventLoopProxy, @@ -147,19 +148,12 @@ impl Gui { std::mem::take(&mut self.event_queue) } - pub fn on_window_event( + pub(crate) fn on_window_event( &mut self, - window: &Window, - app_state: &RunningAppState, + winit_window: &Window, event: &WindowEvent, ) -> EventResponse { - let mut result = self.context.on_window_event(window, event); - - if app_state.has_active_dialog() { - result.consumed = true; - return result; - } - + let mut result = self.context.on_window_event(winit_window, event); result.consumed &= match event { WindowEvent::CursorMoved { position, .. } => { let scale = Scale::<_, DeviceIndependentPixel, _>::new( @@ -293,7 +287,12 @@ impl Gui { } /// Update the user interface, but do not paint the updated state. - pub fn update(&mut self, winit_window: &Window, state: &RunningAppState) { + pub(crate) fn update( + &mut self, + state: &RunningAppState, + window: &ServoShellWindow, + headed_window: &headed_window::Window, + ) { let Self { rendering_context, context, @@ -305,8 +304,9 @@ impl Gui { .. } = self; + let winit_window = headed_window.winit_window(); context.run(winit_window, |ctx| { - load_pending_favicons(ctx, state, favicon_textures); + load_pending_favicons(ctx, window, favicon_textures); // 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 @@ -416,7 +416,7 @@ impl Gui { ui.available_size(), egui::Layout::left_to_right(egui::Align::Center), |ui| { - for (id, webview) in state.webviews().into_iter() { + for (id, webview) in window.webviews().into_iter() { let favicon = favicon_textures .get(&id) .map(|(_, favicon)| favicon) @@ -439,14 +439,14 @@ impl Gui { let scale = Scale::<_, DeviceIndependentPixel, DevicePixel>::new(ctx.pixels_per_point()); - state.for_each_active_dialog(|dialog| dialog.update(ctx)); + headed_window.for_each_active_dialog(window, |dialog| dialog.update(ctx)); // If the top parts of the GUI changed size, then update the size of the WebView and also // the size of its RenderingContext. let rect = ctx.available_rect(); let size = Size2D::new(rect.width(), rect.height()) * scale; let rect = Box2D::from_origin_and_size(Point2D::origin(), size); - if let Some(webview) = state.focused_webview() && + if let Some(webview) = window.focused_webview() && rect != webview.rect() { webview.move_resize(rect); @@ -466,7 +466,7 @@ impl Gui { .show(|ui| ui.add(Label::new(status_text.clone()).extend())); } - state.repaint_servo_if_necessary(); + window.repaint_webviews(); if let Some(render_to_parent) = rendering_context.render_to_parent_callback() { ctx.layer_painter(LayerId::background()).add(PaintCallback { @@ -485,7 +485,7 @@ impl Gui { } /// Paint the GUI, as of the last update. - pub fn paint(&mut self, window: &Window) { + pub(crate) fn paint(&mut self, window: &Window) { self.rendering_context .parent_context() .prepare_for_rendering(); @@ -495,13 +495,13 @@ impl Gui { /// Updates the location field from the given [`RunningAppState`], unless the user has started /// editing it without clicking Go, returning true iff it has changed (needing an egui update). - fn update_location_in_toolbar(&mut self, state: &RunningAppState) -> bool { + fn update_location_in_toolbar(&mut self, window: &ServoShellWindow) -> bool { // User edited without clicking Go? if self.location_dirty { return false; } - let current_url_string = state + let current_url_string = window .focused_webview() .and_then(|webview| Some(webview.url()?.to_string())); match current_url_string { @@ -517,8 +517,8 @@ impl Gui { self.location_dirty = dirty; } - fn update_load_status(&mut self, state: &RunningAppState) -> bool { - let state_status = state + fn update_load_status(&mut self, window: &ServoShellWindow) -> bool { + let state_status = window .focused_webview() .map(|webview| webview.load_status()) .unwrap_or(LoadStatus::Complete); @@ -526,16 +526,16 @@ impl Gui { old_status != self.load_status } - fn update_status_text(&mut self, state: &RunningAppState) -> bool { - let state_status = state + fn update_status_text(&mut self, window: &ServoShellWindow) -> bool { + let state_status = window .focused_webview() .and_then(|webview| webview.status_text()); let old_status = std::mem::replace(&mut self.status_text, state_status); old_status != self.status_text } - fn update_can_go_back_and_forward(&mut self, state: &RunningAppState) -> bool { - let (can_go_back, can_go_forward) = state + fn update_can_go_back_and_forward(&mut self, window: &ServoShellWindow) -> bool { + let (can_go_back, can_go_forward) = window .focused_webview() .map(|webview| (webview.can_go_back(), webview.can_go_forward())) .unwrap_or((false, false)); @@ -544,17 +544,17 @@ impl Gui { old_can_go_back != self.can_go_back || old_can_go_forward != self.can_go_forward } - /// Updates all fields taken from the given [`RunningAppState`], such as the location field. + /// Updates all fields taken from the given [`ServoShellWindow`], such as the location field. /// Returns true iff the egui needs an update. - pub fn update_webview_data(&mut self, state: &RunningAppState) -> bool { + pub(crate) fn update_webview_data(&mut self, window: &ServoShellWindow) -> bool { // Note: We must use the "bitwise OR" (|) operator here instead of "logical OR" (||) // because logical OR would short-circuit if any of the functions return true. // We want to ensure that all functions are called. The "bitwise OR" operator // does not short-circuit. - self.update_location_in_toolbar(state) | - self.update_load_status(state) | - self.update_status_text(state) | - self.update_can_go_back_and_forward(state) + self.update_location_in_toolbar(window) | + self.update_load_status(window) | + self.update_status_text(window) | + self.update_can_go_back_and_forward(window) } /// Returns true if a redraw is required after handling the provided event. @@ -619,11 +619,11 @@ fn embedder_image_to_egui_image(image: &Image) -> egui::ColorImage { /// Uploads all favicons that have not yet been processed to the GPU. fn load_pending_favicons( ctx: &egui::Context, - state: &RunningAppState, + window: &ServoShellWindow, texture_cache: &mut HashMap, ) { - for id in state.take_pending_favicon_loads() { - let Some(webview) = state.webview_by_id(id) else { + for id in window.take_pending_favicon_loads() { + let Some(webview) = window.webview_by_id(id) else { continue; }; let Some(favicon) = webview.favicon() else { diff --git a/ports/servoshell/desktop/headed_window.rs b/ports/servoshell/desktop/headed_window.rs index 473b50ac103..f271b777322 100644 --- a/ports/servoshell/desktop/headed_window.rs +++ b/ports/servoshell/desktop/headed_window.rs @@ -17,6 +17,7 @@ use euclid::{Angle, Length, Point2D, Rotation3D, Scale, Size2D, UnknownUnit, Vec use keyboard_types::ShortcutMatcher; use log::{debug, info, warn}; use raw_window_handle::{HasDisplayHandle, HasWindowHandle, RawWindowHandle}; +use servo::base::generic_channel::GenericSender; use servo::servo_geometry::{ DeviceIndependentIntRect, DeviceIndependentPixel, convert_rect_to_css_pixel, }; @@ -25,11 +26,12 @@ use servo::webrender_api::units::{ DeviceIntPoint, DeviceIntRect, DeviceIntSize, DevicePixel, DevicePoint, }; use servo::{ - Cursor, ImeEvent, InputEvent, InputEventId, InputEventResult, InputMethodControl, Key, - KeyState, KeyboardEvent, Modifiers, MouseButton as ServoMouseButton, MouseButtonAction, - MouseButtonEvent, MouseLeftViewportEvent, MouseMoveEvent, NamedKey, OffscreenRenderingContext, - RenderingContext, ScreenGeometry, Theme, TouchEvent, TouchEventType, TouchId, - WebRenderDebugOption, WebView, WheelDelta, WheelEvent, WheelMode, WindowRenderingContext, + AuthenticationRequest, Cursor, EmbedderControl, EmbedderControlId, ImeEvent, InputEvent, + InputEventId, InputEventResult, InputMethodControl, Key, KeyState, KeyboardEvent, Modifiers, + MouseButton as ServoMouseButton, MouseButtonAction, MouseButtonEvent, MouseLeftViewportEvent, + MouseMoveEvent, NamedKey, OffscreenRenderingContext, PermissionRequest, RenderingContext, + ScreenGeometry, Theme, TouchEvent, TouchEventType, TouchId, WebRenderDebugOption, WebView, + WebViewId, WheelDelta, WheelEvent, WheelMode, WindowRenderingContext, }; use url::Url; use winit::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}; @@ -48,18 +50,20 @@ use { objc2_foundation::MainThreadMarker, }; -use super::app_state::RunningAppState; use super::geometry::{winit_position_to_euclid_point, winit_size_to_euclid_size}; use super::keyutils::{CMD_OR_ALT, keyboard_event_from_winit}; -use super::window_trait::{LINE_HEIGHT, LINE_WIDTH, WindowPortsMethods}; use crate::desktop::accelerated_gl_media::setup_gl_accelerated_media; +use crate::desktop::dialog::Dialog; use crate::desktop::event_loop::AppEvent; use crate::desktop::gui::{Gui, GuiCommand}; use crate::desktop::keyutils::CMD_OR_CONTROL; -use crate::desktop::window_trait::MIN_WINDOW_INNER_SIZE; use crate::parser::location_bar_input_to_url; use crate::prefs::ServoShellPreferences; -use crate::running_app_state::RunningAppStateTrait; +use crate::running_app_state::RunningAppState; +use crate::window::{ + LINE_HEIGHT, LINE_WIDTH, MIN_WINDOW_INNER_SIZE, PlatformWindow, ServoShellWindow, + ServoShellWindowId, +}; pub(crate) const INITIAL_WINDOW_TITLE: &str = "Servo"; @@ -77,7 +81,6 @@ pub struct Window { device_pixel_ratio_override: Option, xr_window_poses: RefCell>>, modifiers_state: Cell, - /// The `RenderingContext` of Servo itself. This is used to render Servo results /// temporarily until they can be blitted into the egui scene. rendering_context: Rc, @@ -92,24 +95,26 @@ pub struct Window { /// When these are handled, they will optionally be used to trigger keybindings that /// are overridable by web content. pending_keyboard_events: RefCell>, - // Keep this as the last field of the struct to ensure that the rendering context is // dropped first. // (https://github.com/servo/servo/issues/36711) winit_window: winit::window::Window, - /// The last title set on this window. We need to store this value here, as `winit::Window::title` /// is not supported very many platforms. last_title: RefCell, + /// The current set of open dialogs. + dialogs: RefCell>>, + /// A list of showing [`InputMethod`] interfaces. + visible_input_methods: RefCell>, } impl Window { - pub fn new( + pub(crate) fn new( servoshell_preferences: &ServoShellPreferences, event_loop: &ActiveEventLoop, event_loop_proxy: EventLoopProxy, initial_url: ServoUrl, - ) -> Window { + ) -> Rc { let no_native_titlebar = servoshell_preferences.no_native_titlebar; let inner_size = servoshell_preferences.initial_window_size; let window_attr = winit::window::Window::default_attributes() @@ -193,7 +198,7 @@ impl Window { )); debug!("Created window {:?}", winit_window.id()); - Window { + Rc::new(Window { gui, winit_window, webview_relative_mouse_point: Cell::new(Point2D::zero()), @@ -211,18 +216,29 @@ impl Window { pending_keyboard_events: Default::default(), rendering_context, last_title: RefCell::new(String::from(INITIAL_WINDOW_TITLE)), - } + dialogs: Default::default(), + visible_input_methods: Default::default(), + }) } - fn handle_keyboard_input(&self, state: Rc, winit_event: KeyEvent) { + pub(crate) fn winit_window(&self) -> &winit::window::Window { + &self.winit_window + } + + fn handle_keyboard_input( + &self, + state: Rc, + window: &ServoShellWindow, + winit_event: KeyEvent, + ) { // First, handle servoshell key bindings that are not overridable by, or visible to, the page. let keyboard_event = keyboard_event_from_winit(&winit_event, self.modifiers_state.get()); - if self.handle_intercepted_key_bindings(state.clone(), &keyboard_event) { + if self.handle_intercepted_key_bindings(state.clone(), window, &keyboard_event) { return; } // Then we deliver character and keyboard events to the page in the focused webview. - let Some(webview) = state.focused_webview() else { + let Some(webview) = window.focused_webview() else { return; }; @@ -316,9 +332,10 @@ impl Window { fn handle_intercepted_key_bindings( &self, state: Rc, + window: &ServoShellWindow, key_event: &KeyboardEvent, ) -> bool { - let Some(focused_webview) = state.focused_webview() else { + let Some(focused_webview) = window.focused_webview() else { return false; }; @@ -326,7 +343,7 @@ impl Window { ShortcutMatcher::from_event(key_event.event.clone()) .shortcut(CMD_OR_CONTROL, 'R', || focused_webview.reload()) .shortcut(CMD_OR_CONTROL, 'W', || { - state.close_webview(focused_webview.id()); + window.close_webview(focused_webview.id()); }) .shortcut(CMD_OR_CONTROL, 'P', || { let rate = env::var("SAMPLING_RATE") @@ -396,34 +413,35 @@ impl Window { || focused_webview.exit_fullscreen(), ) // Select the first 8 tabs via shortcuts - .shortcut(CMD_OR_CONTROL, '1', || state.focus_webview_by_index(0)) - .shortcut(CMD_OR_CONTROL, '2', || state.focus_webview_by_index(1)) - .shortcut(CMD_OR_CONTROL, '3', || state.focus_webview_by_index(2)) - .shortcut(CMD_OR_CONTROL, '4', || state.focus_webview_by_index(3)) - .shortcut(CMD_OR_CONTROL, '5', || state.focus_webview_by_index(4)) - .shortcut(CMD_OR_CONTROL, '6', || state.focus_webview_by_index(5)) - .shortcut(CMD_OR_CONTROL, '7', || state.focus_webview_by_index(6)) - .shortcut(CMD_OR_CONTROL, '8', || state.focus_webview_by_index(7)) + .shortcut(CMD_OR_CONTROL, '1', || window.focus_webview_by_index(0)) + .shortcut(CMD_OR_CONTROL, '2', || window.focus_webview_by_index(1)) + .shortcut(CMD_OR_CONTROL, '3', || window.focus_webview_by_index(2)) + .shortcut(CMD_OR_CONTROL, '4', || window.focus_webview_by_index(3)) + .shortcut(CMD_OR_CONTROL, '5', || window.focus_webview_by_index(4)) + .shortcut(CMD_OR_CONTROL, '6', || window.focus_webview_by_index(5)) + .shortcut(CMD_OR_CONTROL, '7', || window.focus_webview_by_index(6)) + .shortcut(CMD_OR_CONTROL, '8', || window.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 = state.webviews().len(); + let len = window.webviews().len(); if len > 0 { - state.focus_webview_by_index(len - 1) + window.focus_webview_by_index(len - 1) } }) .shortcut(Modifiers::CONTROL, Key::Named(NamedKey::PageDown), || { - if let Some(index) = state.get_focused_webview_index() { - state.focus_webview_by_index((index + 1) % state.webviews().len()) + if let Some(index) = window.get_focused_webview_index() { + window.focus_webview_by_index((index + 1) % window.webviews().len()) } }) .shortcut(Modifiers::CONTROL, Key::Named(NamedKey::PageUp), || { - if let Some(index) = state.get_focused_webview_index() { - let len = state.webviews().len(); - state.focus_webview_by_index((index + len - 1) % len); + if let Some(index) = window.get_focused_webview_index() { + let len = window.webviews().len(); + window.focus_webview_by_index((index + len - 1) % len); } }) .shortcut(CMD_OR_CONTROL, 'T', || { - state.create_and_focus_toplevel_webview( + window.create_and_focus_toplevel_webview( + state.clone(), Url::parse("servo:newtab") .expect("Should be able to unconditionally parse 'servo:newtab' as URL"), ); @@ -450,7 +468,7 @@ impl Window { } /// Takes any events generated during `egui` updates and performs their actions. - fn handle_servoshell_ui_events(&self, state: Rc) { + fn handle_servoshell_ui_events(&self, state: Rc, window: &ServoShellWindow) { let mut gui = self.gui.borrow_mut(); for event in gui.take_commands() { match event { @@ -458,52 +476,122 @@ impl Window { gui.update_location_dirty(false); let Some(url) = location_bar_input_to_url( &location.clone(), - &state.servoshell_preferences().searchpage, + &state.servoshell_preferences.searchpage, ) else { warn!("failed to parse location"); break; }; - if let Some(focused_webview) = state.focused_webview() { + if let Some(focused_webview) = window.focused_webview() { focused_webview.load(url.into_url()); } }, GuiCommand::Back => { - if let Some(focused_webview) = state.focused_webview() { + if let Some(focused_webview) = window.focused_webview() { focused_webview.go_back(1); } }, GuiCommand::Forward => { - if let Some(focused_webview) = state.focused_webview() { + if let Some(focused_webview) = window.focused_webview() { focused_webview.go_forward(1); } }, GuiCommand::Reload => { gui.update_location_dirty(false); - if let Some(focused_webview) = state.focused_webview() { + if let Some(focused_webview) = window.focused_webview() { focused_webview.reload(); } }, GuiCommand::ReloadAll => { gui.update_location_dirty(false); - for (_, webview) in state.webviews() { + for (_, webview) in window.webviews() { webview.reload(); } }, GuiCommand::NewWebView => { gui.update_location_dirty(false); let url = Url::parse("servo:newtab").expect("Should always be able to parse"); - state.create_and_focus_toplevel_webview(url); + window.create_and_focus_toplevel_webview(state.clone(), url); }, GuiCommand::CloseWebView(id) => { gui.update_location_dirty(false); - state.close_webview(id); + window.close_webview(id); }, } } } + + fn show_ime(&self, input_method: InputMethodControl) { + let position = input_method.position(); + self.winit_window.set_ime_allowed(true); + self.winit_window.set_ime_cursor_area( + LogicalPosition::new( + position.min.x, + position.min.y + (self.toolbar_height().0 as i32), + ), + LogicalSize::new( + position.max.x - position.min.x, + position.max.y - position.min.y, + ), + ); + } + + pub(crate) fn for_each_active_dialog( + &self, + window: &ServoShellWindow, + callback: impl Fn(&mut Dialog) -> bool, + ) { + let Some(focused_webview) = window.focused_webview() else { + return; + }; + let mut dialogs = self.dialogs.borrow_mut(); + let Some(dialogs) = dialogs.get_mut(&focused_webview.id()) else { + return; + }; + if dialogs.is_empty() { + return; + } + + // If a dialog is open, clear any Servo cursor. TODO: This should restore the + // cursor too, when all dialogs close. In general, we need a better cursor + // management strategy. + self.set_cursor(Cursor::Default); + + let length = dialogs.len(); + dialogs.retain_mut(callback); + if length != dialogs.len() { + window.set_needs_repaint(); + } + } + + fn add_dialog(&self, webview_id: WebViewId, dialog: Dialog) { + self.dialogs + .borrow_mut() + .entry(webview_id) + .or_default() + .push(dialog) + } + + fn remove_dialog(&self, webview_id: WebViewId, embedder_control_id: EmbedderControlId) { + let mut dialogs = self.dialogs.borrow_mut(); + if let Some(dialogs) = dialogs.get_mut(&webview_id) { + dialogs.retain(|dialog| dialog.embedder_control_id() != Some(embedder_control_id)); + } + dialogs.retain(|_, dialogs| !dialogs.is_empty()); + } + + fn has_active_dialog_for_webview(&self, webview_id: WebViewId) -> bool { + // First lazily clean up any empty dialog vectors. + let mut dialogs = self.dialogs.borrow_mut(); + dialogs.retain(|_, dialogs| !dialogs.is_empty()); + dialogs.contains_key(&webview_id) + } + + fn toolbar_height(&self) -> Length { + self.gui.borrow().toolbar_height() + } } -impl WindowPortsMethods for Window { +impl PlatformWindow for Window { fn screen_geometry(&self) -> ScreenGeometry { let hidpi_factor = self.hidpi_scale_factor(); let toolbar_size = Size2D::new(0.0, (self.toolbar_height() * self.hidpi_scale_factor()).0); @@ -536,12 +624,12 @@ impl WindowPortsMethods for Window { .unwrap_or_else(|| self.device_hidpi_scale_factor()) } - fn rebuild_user_interface(&self, state: &RunningAppState) { - self.gui.borrow_mut().update(&self.winit_window, state); + fn rebuild_user_interface(&self, state: &RunningAppState, window: &ServoShellWindow) { + self.gui.borrow_mut().update(state, window, self); } - fn update_user_interface_state(&self, state: &RunningAppState) -> bool { - let title = state + fn update_user_interface_state(&self, _: &RunningAppState, window: &ServoShellWindow) -> bool { + let title = window .focused_webview() .and_then(|webview| { webview @@ -556,15 +644,21 @@ impl WindowPortsMethods for Window { *self.last_title.borrow_mut() = title; } - self.gui.borrow_mut().update_webview_data(state) + self.gui.borrow_mut().update_webview_data(window) } - fn handle_winit_window_event(&self, state: Rc, event: WindowEvent) { + fn handle_winit_window_event( + &self, + state: Rc, + window: &ServoShellWindow, + event: WindowEvent, + ) { if event == WindowEvent::RedrawRequested { // WARNING: do not defer painting or presenting to some later tick of the event // loop or servoshell may become unresponsive! (servo#30312) + let _ = self.rendering_context().make_current(); let mut gui = self.gui.borrow_mut(); - gui.update(&self.winit_window, &state); + gui.update(&state, window, self); gui.paint(&self.winit_window); } @@ -587,20 +681,20 @@ impl WindowPortsMethods for Window { .borrow() .set_zoom_factor(effective_egui_zoom_factor); - state.hidpi_scale_factor_changed(); + window.hidpi_scale_factor_changed(); // Request a winit redraw event, so we can recomposite, update and paint // the GUI, and present the new frame. self.winit_window.request_redraw(); }, ref event => { - let response = - self.gui - .borrow_mut() - .on_window_event(&self.winit_window, &state, event); + let response = self + .gui + .borrow_mut() + .on_window_event(&self.winit_window, event); if let WindowEvent::Resized(_) = event { - self.rebuild_user_interface(&state); + self.rebuild_user_interface(&state, window); } if response.repaint && *event != WindowEvent::RedrawRequested { @@ -613,6 +707,19 @@ impl WindowPortsMethods for Window { }, } + if matches!( + event, + WindowEvent::CursorMoved { .. } | + WindowEvent::MouseInput { .. } | + WindowEvent::MouseWheel { .. } | + WindowEvent::KeyboardInput { .. } + ) && window + .focused_webview() + .is_some_and(|webview| self.has_active_dialog_for_webview(webview.id())) + { + consumed = true; + } + if !consumed { // Make sure to handle early resize events even when there are no webviews yet if let WindowEvent::Resized(new_inner_size) = event { @@ -625,10 +732,10 @@ impl WindowPortsMethods for Window { } } - if let Some(webview) = state.focused_webview() { + if let Some(webview) = window.focused_webview() { match event { WindowEvent::KeyboardInput { event, .. } => { - self.handle_keyboard_input(state.clone(), event) + self.handle_keyboard_input(state.clone(), window, event) }, WindowEvent::ModifiersChanged(modifiers) => { self.modifiers_state.set(modifiers.state()) @@ -689,7 +796,7 @@ impl WindowPortsMethods for Window { ); }, WindowEvent::CloseRequested => { - state.servo().start_shutting_down(); + window.schedule_close(); }, WindowEvent::ThemeChanged(theme) => { webview.notify_theme_change(match theme { @@ -732,10 +839,15 @@ impl WindowPortsMethods for Window { } // Consume and handle any events from the servoshell UI. - self.handle_servoshell_ui_events(state.clone()); + self.handle_servoshell_ui_events(state.clone(), window); } - fn handle_winit_app_event(&self, state: Rc, app_event: AppEvent) { + fn handle_winit_app_event( + &self, + state: Rc, + window: &ServoShellWindow, + app_event: AppEvent, + ) { if let AppEvent::Accessibility(ref event) = app_event { if self .gui @@ -747,11 +859,20 @@ impl WindowPortsMethods for Window { } // Consume and handle any events from user interface interaction. - self.handle_servoshell_ui_events(state.clone()); + self.handle_servoshell_ui_events(state.clone(), window); } - fn request_repaint(&self, _: &RunningAppState) { + fn request_repaint(&self, window: &ServoShellWindow) { self.winit_window.request_redraw(); + + // FIXME: This is a workaround for dialogs, which do not seem to animate, unless we + // constantly repaint the egui scene. + if window + .focused_webview() + .is_some_and(|webview| self.has_active_dialog_for_webview(webview.id())) + { + window.set_needs_repaint(); + } } fn request_resize(&self, _: &WebView, new_outer_size: DeviceIntSize) -> Option { @@ -875,8 +996,9 @@ impl WindowPortsMethods for Window { self.winit_window.set_cursor_visible(true); } - fn id(&self) -> winit::window::WindowId { - self.winit_window.id() + fn id(&self) -> ServoShellWindowId { + let id: u64 = self.winit_window.id().into(); + id.into() } #[cfg(feature = "webxr")] @@ -903,33 +1025,10 @@ impl WindowPortsMethods for Window { Rc::new(XRWindow { winit_window, pose }) } - fn toolbar_height(&self) -> Length { - self.gui.borrow().toolbar_height() - } - fn rendering_context(&self) -> Rc { self.rendering_context.clone() } - fn show_ime(&self, input_method: InputMethodControl) { - let position = input_method.position(); - self.winit_window.set_ime_allowed(true); - self.winit_window.set_ime_cursor_area( - LogicalPosition::new( - position.min.x, - position.min.y + (self.toolbar_height().0 as i32), - ), - LogicalSize::new( - position.max.x - position.min.x, - position.max.y - position.min.y, - ), - ); - } - - fn hide_ime(&self) { - self.winit_window.set_ime_allowed(false); - } - fn theme(&self) -> servo::Theme { match self.winit_window.theme() { Some(winit::window::Theme::Dark) => servo::Theme::Dark, @@ -969,6 +1068,96 @@ impl WindowPortsMethods for Window { webview.set_page_zoom(1.0); }); } + + fn focused(&self) -> bool { + self.winit_window.has_focus() + } + + fn show_embedder_control(&self, webview_id: WebViewId, embedder_control: EmbedderControl) { + let control_id = embedder_control.id(); + match embedder_control { + EmbedderControl::SelectElement(prompt) => { + // FIXME: Reading the toolbar height is needed here to properly position the select dialog. + // But if the toolbar height changes while the dialog is open then the position won't be updated + let offset = self.gui.borrow().toolbar_height(); + self.add_dialog( + webview_id, + Dialog::new_select_element_dialog(prompt, offset), + ); + }, + EmbedderControl::ColorPicker(color_picker) => { + // FIXME: Reading the toolbar height is needed here to properly position the select dialog. + // But if the toolbar height changes while the dialog is open then the position won't be updated + let offset = self.gui.borrow().toolbar_height(); + self.add_dialog( + webview_id, + Dialog::new_color_picker_dialog(color_picker, offset), + ); + }, + EmbedderControl::InputMethod(input_method_control) => { + self.visible_input_methods.borrow_mut().push(control_id); + self.show_ime(input_method_control); + }, + EmbedderControl::FilePicker(file_picker) => { + self.add_dialog(webview_id, Dialog::new_file_dialog(file_picker)); + }, + EmbedderControl::SimpleDialog(simple_dialog) => { + self.add_dialog(webview_id, Dialog::new_simple_dialog(simple_dialog)); + }, + EmbedderControl::ContextMenu(prompt) => { + let offset = self.gui.borrow().toolbar_height(); + self.add_dialog(webview_id, Dialog::new_context_menu(prompt, offset)); + }, + } + } + + fn hide_embedder_control(&self, webview_id: WebViewId, embedder_control_id: EmbedderControlId) { + { + let mut visible_input_methods = self.visible_input_methods.borrow_mut(); + if let Some(index) = visible_input_methods + .iter() + .position(|visible_id| *visible_id == embedder_control_id) + { + visible_input_methods.remove(index); + self.winit_window.set_ime_allowed(false); + } + } + self.remove_dialog(webview_id, embedder_control_id); + } + + fn show_bluetooth_device_dialog( + &self, + webview_id: WebViewId, + devices: Vec, + response_sender: GenericSender>, + ) { + self.add_dialog( + webview_id, + Dialog::new_device_selection_dialog(devices, response_sender), + ); + } + + fn show_permission_dialog(&self, webview_id: WebViewId, permission_request: PermissionRequest) { + self.add_dialog( + webview_id, + Dialog::new_permission_request_dialog(permission_request), + ); + } + + fn show_http_authentication_dialog( + &self, + webview_id: WebViewId, + authentication_request: AuthenticationRequest, + ) { + self.add_dialog( + webview_id, + Dialog::new_authentication_dialog(authentication_request), + ); + } + + fn dismiss_embedder_controls_for_webview(&self, webview_id: WebViewId) { + self.dialogs.borrow_mut().remove(&webview_id); + } } fn winit_phase_to_touch_event_type(phase: TouchPhase) -> TouchEventType { diff --git a/ports/servoshell/desktop/headless_window.rs b/ports/servoshell/desktop/headless_window.rs index 04eaef37f45..0b5ad27f1ce 100644 --- a/ports/servoshell/desktop/headless_window.rs +++ b/ports/servoshell/desktop/headless_window.rs @@ -10,8 +10,7 @@ use std::cell::Cell; use std::rc::Rc; -use euclid::num::Zero; -use euclid::{Length, Point2D, Scale, Size2D}; +use euclid::{Point2D, Scale, Size2D}; use servo::servo_geometry::{ DeviceIndependentIntRect, DeviceIndependentPixel, convert_rect_to_css_pixel, }; @@ -19,9 +18,8 @@ use servo::webrender_api::units::{DeviceIntPoint, DeviceIntRect, DeviceIntSize, use servo::{RenderingContext, ScreenGeometry, SoftwareRenderingContext, WebView}; use winit::dpi::PhysicalSize; -use super::app_state::RunningAppState; -use crate::desktop::window_trait::{MIN_WINDOW_INNER_SIZE, WindowPortsMethods}; use crate::prefs::ServoShellPreferences; +use crate::window::{MIN_WINDOW_INNER_SIZE, PlatformWindow, ServoShellWindow, ServoShellWindowId}; pub struct Window { fullscreen: Cell, @@ -34,8 +32,7 @@ pub struct Window { } impl Window { - #[allow(clippy::new_ret_no_self)] - pub fn new(servoshell_preferences: &ServoShellPreferences) -> Rc { + pub fn new(servoshell_preferences: &ServoShellPreferences) -> Rc { let size = servoshell_preferences.initial_window_size; let device_pixel_ratio_override = servoshell_preferences.device_pixel_ratio_override; @@ -67,9 +64,9 @@ impl Window { } } -impl WindowPortsMethods for Window { - fn id(&self) -> winit::window::WindowId { - winit::window::WindowId::dummy() +impl PlatformWindow for Window { + fn id(&self) -> ServoShellWindowId { + 0.into() } fn screen_geometry(&self) -> servo::ScreenGeometry { @@ -87,8 +84,8 @@ impl WindowPortsMethods for Window { self.window_position.set(point); } - fn request_repaint(&self, state: &RunningAppState) { - state.repaint_servo_if_necessary(); + fn request_repaint(&self, window: &ServoShellWindow) { + window.repaint_webviews(); } fn request_resize(&self, webview: &WebView, new_size: DeviceIntSize) -> Option { @@ -138,10 +135,6 @@ impl WindowPortsMethods for Window { unimplemented!() } - fn toolbar_height(&self) -> Length { - Length::zero() - } - fn window_rect(&self) -> DeviceIndependentIntRect { convert_rect_to_css_pixel( DeviceIntRect::from_origin_and_size(self.window_position.get(), self.inner_size.get()), @@ -165,4 +158,8 @@ impl WindowPortsMethods for Window { self.screen_size.height as u32, )); } + + fn focused(&self) -> bool { + true + } } diff --git a/ports/servoshell/desktop/mod.rs b/ports/servoshell/desktop/mod.rs index a5a3181cd79..1379f413b64 100644 --- a/ports/servoshell/desktop/mod.rs +++ b/ports/servoshell/desktop/mod.rs @@ -6,11 +6,10 @@ mod accelerated_gl_media; pub(crate) mod app; -mod app_state; pub(crate) mod cli; -mod dialog; +pub(crate) mod dialog; pub(crate) mod event_loop; -mod gamepad; +pub(crate) mod gamepad; pub mod geometry; mod gui; mod headed_window; @@ -20,4 +19,3 @@ mod protocols; mod tracing; #[cfg(feature = "webxr")] mod webxr; -mod window_trait; diff --git a/ports/servoshell/desktop/webxr.rs b/ports/servoshell/desktop/webxr.rs index 9990f53908f..7b525b18598 100644 --- a/ports/servoshell/desktop/webxr.rs +++ b/ports/servoshell/desktop/webxr.rs @@ -15,7 +15,7 @@ use servo::webxr::glwindow::GlWindowDiscovery; use servo::webxr::openxr::{AppInfo, OpenXrDiscovery}; use winit::event_loop::ActiveEventLoop; -use super::window_trait::WindowPortsMethods; +use crate::window::PlatformWindow; enum XrDiscovery { GlWindow(GlWindowDiscovery), @@ -29,7 +29,7 @@ pub(crate) struct XrDiscoveryWebXrRegistry { impl XrDiscoveryWebXrRegistry { pub(crate) fn new_boxed( - window: Rc, + window: Rc, event_loop: Option<&ActiveEventLoop>, preferences: &Preferences, ) -> Box { diff --git a/ports/servoshell/desktop/window_trait.rs b/ports/servoshell/desktop/window_trait.rs deleted file mode 100644 index ac2b9f44847..00000000000 --- a/ports/servoshell/desktop/window_trait.rs +++ /dev/null @@ -1,92 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ - -//! Definition of Window. -//! Implemented by headless and headed windows. - -use std::rc::Rc; - -use euclid::{Length, Scale}; -use servo::servo_geometry::{DeviceIndependentIntRect, DeviceIndependentPixel}; -use servo::webrender_api::units::{DeviceIntPoint, DeviceIntSize, DevicePixel}; -use servo::{ - Cursor, InputEventId, InputEventResult, InputMethodControl, RenderingContext, ScreenGeometry, - WebView, -}; -use winit::event::WindowEvent; - -use super::app_state::RunningAppState; -use crate::desktop::event_loop::AppEvent; - -// This should vary by zoom level and maybe actual text size (focused or under cursor) -pub(crate) const LINE_HEIGHT: f32 = 76.0; -pub(crate) const LINE_WIDTH: f32 = 76.0; - -/// -/// "A window size of 10x10px shouldn't be supported by any browser." -pub(crate) const MIN_WINDOW_INNER_SIZE: DeviceIntSize = DeviceIntSize::new(100, 100); - -pub trait WindowPortsMethods { - fn id(&self) -> winit::window::WindowId; - fn screen_geometry(&self) -> ScreenGeometry; - fn device_hidpi_scale_factor(&self) -> Scale; - fn hidpi_scale_factor(&self) -> Scale; - 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. - fn rebuild_user_interface(&self, _: &RunningAppState) {} - /// 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) -> bool { - false - } - /// Handle a winit [`WindowEvent`]. Returns `true` if the event loop should continue - /// and `false` otherwise. - /// - /// TODO: This should be handled internally in the winit window if possible so that it - /// makes more sense when we are mixing headed and headless windows. - fn handle_winit_window_event(&self, _: Rc, _: WindowEvent) {} - /// Handle a winit [`AppEvent`]. Returns `true` if the event loop should continue and - /// `false` otherwise. - /// - /// TODO: This should be handled internally in the winit window if possible so that it - /// makes more sense when we are mixing headed and headless windows. - fn handle_winit_app_event(&self, _: Rc, _: AppEvent) {} - /// 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, _: &RunningAppState); - /// 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(feature = "webxr")] - fn new_glwindow( - &self, - event_loop: &winit::event_loop::ActiveEventLoop, - ) -> Rc; - fn toolbar_height(&self) -> Length; - /// This returns [`RenderingContext`] matching the viewport. - fn rendering_context(&self) -> Rc; - fn show_ime(&self, _input_method: InputMethodControl) {} - fn hide_ime(&self) {} - fn theme(&self) -> servo::Theme { - servo::Theme::Light - } - fn window_rect(&self) -> DeviceIndependentIntRect; - fn maximize(&self, webview: &WebView); - - fn notify_input_event_handled( - &self, - _webview: &WebView, - _id: InputEventId, - _result: InputEventResult, - ) { - } -} diff --git a/ports/servoshell/egl/android.rs b/ports/servoshell/egl/android/mod.rs similarity index 83% rename from ports/servoshell/egl/android.rs rename to ports/servoshell/egl/android/mod.rs index 10f7f5a8ee1..b4ff9c62262 100644 --- a/ports/servoshell/egl/android.rs +++ b/ports/servoshell/egl/android/mod.rs @@ -5,31 +5,52 @@ #![allow(non_snake_case)] mod resources; -mod simpleservo; +use std::cell::RefCell; +use std::mem; use std::os::raw::{c_char, c_int, c_void}; use std::ptr::NonNull; +use std::rc::Rc; use std::sync::Arc; use android_logger::{self, Config, FilterBuilder}; +use dpi::PhysicalSize; +use euclid::{Point2D, Rect, Size2D}; use jni::objects::{GlobalRef, JClass, JObject, JString, JValue, JValueOwned}; use jni::sys::{jboolean, jfloat, jint, jobject}; use jni::{JNIEnv, JavaVM}; use keyboard_types::{Key, NamedKey}; use log::{debug, error, info, warn}; use raw_window_handle::{ - AndroidDisplayHandle, AndroidNdkWindowHandle, RawDisplayHandle, RawWindowHandle, + AndroidDisplayHandle, AndroidNdkWindowHandle, DisplayHandle, RawDisplayHandle, RawWindowHandle, + WindowHandle, }; +use resources::ResourceReaderInstance; +use servo::webrender_api::units::DevicePixel; use servo::{ - AlertResponse, EventLoopWaker, InputMethodControl, LoadStatus, MediaSessionActionType, - MouseButton, PermissionRequest, PrefValue, SimpleDialog, WebView, + self, EventLoopWaker, InputMethodControl, LoadStatus, MediaSessionActionType, MouseButton, + PrefValue, }; -use simpleservo::{APP, InitOptions, MediaSessionPlaybackState}; +pub use servo::{MediaSessionPlaybackState, WindowRenderingContext}; -use super::app_state::{Coordinates, RunningAppState}; +use super::app::{App, AppInitOptions, VsyncRefreshDriver}; use super::host_trait::HostTrait; -use crate::prefs::EXPERIMENTAL_PREFS; -use crate::running_app_state::RunningAppStateTrait; +use crate::prefs::{ArgumentParsingResult, EXPERIMENTAL_PREFS, parse_command_line_arguments}; + +thread_local! { + pub static APP: RefCell>> = const { RefCell::new(None) }; +} + +struct InitOptions { + args: Vec, + url: Option, + viewport_rect: Rect, + density: f32, + #[cfg(feature = "webxr")] + xr_discovery: Option, + window_handle: RawWindowHandle, + display_handle: RawDisplayHandle, +} struct HostCallbacks { callbacks: GlobalRef, @@ -51,10 +72,10 @@ pub extern "C" fn android_main() { fn call(env: &mut JNIEnv, f: F) where - F: FnOnce(&RunningAppState), + F: FnOnce(&App), { APP.with(|app| match app.borrow().as_ref() { - Some(ref app_state) => (f)(app_state), + Some(app) => (f)(app), None => throw(env, "Servo not available in this thread"), }); } @@ -64,11 +85,13 @@ pub extern "C" fn Java_org_servo_servoview_JNIServo_version<'local>( env: JNIEnv<'local>, _class: JClass<'local>, ) -> JString<'local> { - let v = crate::VERSION; - env.new_string(&v) + let version = crate::VERSION; + env.new_string(version) .unwrap_or_else(|_str| JObject::null().into()) } +/// Initialize Servo. At that point, we need a valid GL context. In the future, this will +/// be done in multiple steps. #[unsafe(no_mangle)] pub extern "C" fn Java_org_servo_servoview_JNIServo_init<'local>( mut env: JNIEnv<'local>, @@ -78,7 +101,8 @@ pub extern "C" fn Java_org_servo_servoview_JNIServo_init<'local>( callbacks_obj: JObject<'local>, surface: JObject<'local>, ) { - let (opts, log, log_str, _gst_debug_str) = match get_options(&mut env, &opts, &surface) { + let (mut init_opts, log, log_str, _gst_debug_str) = match get_options(&mut env, &opts, &surface) + { Ok((opts, log, log_str, gst_debug_str)) => (opts, log, log_str, gst_debug_str), Err(err) => { throw(&mut env, &err); @@ -141,12 +165,68 @@ pub extern "C" fn Java_org_servo_servoview_JNIServo_init<'local>( }, }; - let wakeup = Box::new(WakeupCallback::new(callbacks_ref.clone(), &env)); - let callbacks = Box::new(HostCallbacks::new(callbacks_ref, &env)); + let event_loop_waker = Box::new(WakeupCallback::new(callbacks_ref.clone(), &env)); + let host = Box::new(HostCallbacks::new(callbacks_ref, &env)); - if let Err(err) = simpleservo::init(opts, wakeup, callbacks) { - throw(&mut env, err) + crate::init_crypto(); + servo::resources::set(Box::new(ResourceReaderInstance::new())); + + // `parse_command_line_arguments` expects the first argument to be the binary name. + let mut args = mem::take(&mut init_opts.args); + args.insert(0, "servo".to_string()); + + let (opts, mut preferences, servoshell_preferences) = match parse_command_line_arguments(args) { + ArgumentParsingResult::ContentProcess(..) => { + unreachable!("Android does not have support for multiprocess yet.") + }, + ArgumentParsingResult::ChromeProcess(opts, preferences, servoshell_preferences) => { + (opts, preferences, servoshell_preferences) + }, + ArgumentParsingResult::Exit => { + std::process::exit(0); + }, + ArgumentParsingResult::ErrorParsing => std::process::exit(1), }; + + preferences.set_value("viewport_meta_enabled", servo::PrefValue::Bool(true)); + + crate::init_tracing(servoshell_preferences.tracing_filter.as_deref()); + + let (display_handle, window_handle) = unsafe { + ( + DisplayHandle::borrow_raw(init_opts.display_handle), + WindowHandle::borrow_raw(init_opts.window_handle), + ) + }; + + let size = init_opts.viewport_rect.size; + let refresh_driver = Rc::new(VsyncRefreshDriver::default()); + let rendering_context = Rc::new( + WindowRenderingContext::new_with_refresh_driver( + display_handle, + window_handle, + PhysicalSize::new(size.width as u32, size.height as u32), + refresh_driver.clone(), + ) + .expect("Could not create RenderingContext"), + ); + + APP.with(|app| { + *app.borrow_mut() = Some(App::new(AppInitOptions { + host, + event_loop_waker, + viewport_rect: init_opts.viewport_rect, + hidpi_scale_factor: init_opts.density, + rendering_context, + refresh_driver, + initial_url: init_opts.url, + opts, + preferences, + servoshell_preferences, + #[cfg(feature = "webxr")] + xr_discovery: init_opts.xr_discovery, + })); + }); } #[unsafe(no_mangle)] @@ -178,7 +258,9 @@ pub extern "C" fn Java_org_servo_servoview_JNIServo_deinit<'local>( _class: JClass<'local>, ) { debug!("deinit"); - simpleservo::deinit(); + APP.with(|app| { + *app.borrow_mut() = None; + }); } #[unsafe(no_mangle)] @@ -187,10 +269,10 @@ pub extern "C" fn Java_org_servo_servoview_JNIServo_resize<'local>( _: JClass<'local>, coordinates: JObject<'local>, ) { - let coords = jni_coords_to_rust_coords(&mut env, &coordinates); - debug!("resize {:#?}", coords); - match coords { - Ok(coords) => call(&mut env, |s| s.resize(coords.clone())), + let viewport_rect = jni_coordinate_to_rust_viewport_rect(&mut env, &coordinates); + debug!("resize {viewport_rect:#?}"); + match viewport_rect { + Ok(viewport_rect) => call(&mut env, |s| s.resize(viewport_rect)), Err(error) => throw(&mut env, &error), } } @@ -201,9 +283,8 @@ pub extern "C" fn Java_org_servo_servoview_JNIServo_performUpdates<'local>( _class: JClass<'local>, ) { debug!("performUpdates"); - call(&mut env, |s| { - s.perform_updates(); - s.present_if_needed(); + call(&mut env, |app| { + app.spin_event_loop(); }); } @@ -474,14 +555,14 @@ pub extern "C" fn Java_org_servo_servoview_JNIServo_resumeCompositor<'local>( coordinates: JObject<'local>, ) { debug!("resumeCompositor"); - let coords = match jni_coords_to_rust_coords(&mut env, &coordinates) { - Ok(coords) => coords, + let viewport_rect = match jni_coordinate_to_rust_viewport_rect(&mut env, &coordinates) { + Ok(viewport_rect) => viewport_rect, Err(error) => return throw(&mut env, &error), }; let (_, window_handle) = display_and_window_handle(&mut env, &surface); - call(&mut env, |s| { - s.resume_compositor(window_handle, coords.clone()) + call(&mut env, |app| { + app.resume_compositor(window_handle, viewport_rect); }); } @@ -540,7 +621,9 @@ impl HostCallbacks { let jvm = env.get_java_vm().unwrap(); HostCallbacks { callbacks, jvm } } +} +impl HostTrait for HostCallbacks { fn show_alert(&self, message: String) { let mut env = self.jvm.get_env().unwrap(); let Ok(string) = new_string_as_jvalue(&mut env, &message) else { @@ -554,44 +637,6 @@ impl HostCallbacks { ) .unwrap(); } -} - -impl HostTrait for HostCallbacks { - fn request_permission(&self, _webview: WebView, request: PermissionRequest) { - warn!("Permissions prompt not implemented. Denied."); - request.deny(); - } - - fn show_simple_dialog(&self, _webview: WebView, dialog: SimpleDialog) { - let _ = match dialog { - SimpleDialog::Alert { - message, - response_sender, - .. - } => { - debug!("SimpleDialog::Alert"); - // TODO: Indicate that this message is untrusted, and what origin it came from. - self.show_alert(message); - response_sender.send(AlertResponse::Ok) - }, - SimpleDialog::Confirm { - message, - response_sender, - .. - } => { - warn!("Confirm dialog not implemented. Cancelled. {}", message); - response_sender.send(Default::default()) - }, - SimpleDialog::Prompt { - message, - response_sender, - .. - } => { - warn!("Prompt dialog not implemented. Cancelled. {}", message); - response_sender.send(Default::default()) - }, - }; - } fn notify_load_status_changed(&self, load_status: LoadStatus) { debug!("notify_load_status_changed: {load_status:?}"); @@ -632,24 +677,6 @@ impl HostTrait for HostCallbacks { .unwrap(); } - fn on_allow_navigation(&self, url: String) -> bool { - debug!("on_allow_navigation"); - let mut env = self.jvm.get_env().unwrap(); - let Ok(url_string) = new_string_as_jvalue(&mut env, &url) else { - return false; - }; - let allow = env.call_method( - self.callbacks.as_obj(), - "onAllowNavigation", - "(Ljava/lang/String;)Z", - &[(&url_string).into()], - ); - match allow { - Ok(allow) => allow.z().unwrap(), - Err(_) => true, - } - } - fn on_url_changed(&self, url: String) { debug!("on_url_changed"); let mut env = self.jvm.get_env().unwrap(); @@ -783,10 +810,10 @@ fn new_string_as_jvalue<'local>( Ok(JValueOwned::from(jstring)) } -fn jni_coords_to_rust_coords<'local>( +fn jni_coordinate_to_rust_viewport_rect<'local>( env: &mut JNIEnv<'local>, obj: &JObject<'local>, -) -> Result { +) -> Result, String> { let x = get_non_null_field(env, obj, "x", "I")? .i() .map_err(|_| "x not an int")? as i32; @@ -799,7 +826,7 @@ fn jni_coords_to_rust_coords<'local>( let height = get_non_null_field(env, obj, "height", "I")? .i() .map_err(|_| "height not an int")? as i32; - Ok(Coordinates::new(x, y, width, height)) + Ok(Rect::new(Point2D::new(x, y), Size2D::new(width, height))) } fn get_field<'local>( @@ -877,7 +904,7 @@ fn get_options<'local>( )? .l() .map_err(|_| "coordinates is not an object")?; - let coordinates = jni_coords_to_rust_coords(env, &coordinates)?; + let viewport_rect = jni_coordinate_to_rust_viewport_rect(env, &coordinates)?; let mut args: Vec = match args { Some(args) => serde_json::from_str(&args) @@ -893,7 +920,7 @@ fn get_options<'local>( let opts = InitOptions { args, url, - coordinates, + viewport_rect, density, xr_discovery: None, window_handle, diff --git a/ports/servoshell/egl/android/simpleservo.rs b/ports/servoshell/egl/android/simpleservo.rs deleted file mode 100644 index c3f8ac90d28..00000000000 --- a/ports/servoshell/egl/android/simpleservo.rs +++ /dev/null @@ -1,138 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ - -use std::cell::RefCell; -use std::mem; -use std::rc::Rc; - -use crossbeam_channel::unbounded; -use dpi::PhysicalSize; -use raw_window_handle::{DisplayHandle, RawDisplayHandle, RawWindowHandle, WindowHandle}; -use servo::{self, EventLoopWaker, ServoBuilder, resources}; -pub use servo::{MediaSessionPlaybackState, WindowRenderingContext}; - -use crate::egl::android::resources::ResourceReaderInstance; -#[cfg(feature = "webxr")] -use crate::egl::app_state::XrDiscoveryWebXrRegistry; -use crate::egl::app_state::{ - Coordinates, RunningAppState, ServoWindowCallbacks, VsyncRefreshDriver, -}; -use crate::egl::host_trait::HostTrait; -use crate::prefs::{ArgumentParsingResult, parse_command_line_arguments}; - -thread_local! { - pub static APP: RefCell>> = const { RefCell::new(None) }; -} - -pub struct InitOptions { - pub args: Vec, - pub url: Option, - pub coordinates: Coordinates, - pub density: f32, - #[cfg(feature = "webxr")] - pub xr_discovery: Option, - pub window_handle: RawWindowHandle, - pub display_handle: RawDisplayHandle, -} - -/// Initialize Servo. At that point, we need a valid GL context. -/// In the future, this will be done in multiple steps. -pub fn init( - mut init_opts: InitOptions, - waker: Box, - callbacks: Box, -) -> Result<(), &'static str> { - crate::init_crypto(); - resources::set(Box::new(ResourceReaderInstance::new())); - - // `parse_command_line_arguments` expects the first argument to be the binary name. - let mut args = mem::take(&mut init_opts.args); - args.insert(0, "servo".to_string()); - - let (opts, mut preferences, servoshell_preferences) = match parse_command_line_arguments(args) { - ArgumentParsingResult::ContentProcess(..) => { - unreachable!("Android does not have support for multiprocess yet.") - }, - ArgumentParsingResult::ChromeProcess(opts, preferences, servoshell_preferences) => { - (opts, preferences, servoshell_preferences) - }, - ArgumentParsingResult::Exit => { - std::process::exit(0); - }, - ArgumentParsingResult::ErrorParsing => std::process::exit(1), - }; - - preferences.set_value("viewport_meta_enabled", servo::PrefValue::Bool(true)); - - crate::init_tracing(servoshell_preferences.tracing_filter.as_deref()); - - let (display_handle, window_handle) = unsafe { - ( - DisplayHandle::borrow_raw(init_opts.display_handle), - WindowHandle::borrow_raw(init_opts.window_handle), - ) - }; - - let size = init_opts.coordinates.viewport.size; - let refresh_driver = Rc::new(VsyncRefreshDriver::default()); - let rendering_context = Rc::new( - WindowRenderingContext::new_with_refresh_driver( - display_handle, - window_handle, - PhysicalSize::new(size.width as u32, size.height as u32), - refresh_driver.clone(), - ) - .expect("Could not create RenderingContext"), - ); - - let window_callbacks = Rc::new(ServoWindowCallbacks::new( - callbacks, - RefCell::new(init_opts.coordinates), - )); - - let servo_builder = ServoBuilder::default() - .opts(opts) - .preferences(preferences) - .event_loop_waker(waker.clone()); - - #[cfg(feature = "webxr")] - let servo_builder = servo_builder.webxr_registry(Box::new(XrDiscoveryWebXrRegistry::new( - init_opts.xr_discovery, - ))); - - let servo = servo_builder.build(); - - // Initialize WebDriver server if port is specified - let webdriver_receiver = servoshell_preferences.webdriver_port.map(|port| { - let (embedder_sender, embedder_receiver) = unbounded(); - webdriver_server::start_server(port, embedder_sender, waker); - log::info!("WebDriver server started on port {port}"); - embedder_receiver - }); - - APP.with(|app| { - let app_state = RunningAppState::new( - init_opts.url, - init_opts.density, - rendering_context, - servo, - window_callbacks, - Some(refresh_driver), - servoshell_preferences, - webdriver_receiver, - ); - *app.borrow_mut() = Some(app_state); - }); - - Ok(()) -} - -pub fn deinit() { - APP.with(|app| { - let app = app.replace(None).unwrap(); - if let Some(app_state) = Rc::into_inner(app) { - app_state.deinit() - } - }); -} diff --git a/ports/servoshell/egl/app.rs b/ports/servoshell/egl/app.rs new file mode 100644 index 00000000000..c873e7e2566 --- /dev/null +++ b/ports/servoshell/egl/app.rs @@ -0,0 +1,662 @@ +/* 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 dpi::PhysicalSize; +use euclid::{Rect, Scale}; +use keyboard_types::{CompositionEvent, CompositionState, Key, KeyState, NamedKey}; +use log::{info, warn}; +use raw_window_handle::{RawWindowHandle, WindowHandle}; +use servo::base::id::WebViewId; +use servo::config::opts::Opts; +use servo::config::prefs::Preferences; +use servo::servo_geometry::{ + DeviceIndependentIntRect, DeviceIndependentPixel, convert_rect_to_css_pixel, +}; +use servo::webrender_api::units::{DeviceIntSize, DevicePixel, DevicePoint, DeviceVector2D}; +use servo::{ + AlertResponse, EmbedderControl, EmbedderControlId, EventLoopWaker, ImeEvent, InputEvent, + KeyboardEvent, LoadStatus, MediaSessionActionType, MediaSessionEvent, MouseButton, + MouseButtonAction, MouseButtonEvent, MouseMoveEvent, RefreshDriver, RenderingContext, + ScreenGeometry, Scroll, Servo, ServoBuilder, SimpleDialog, TouchEvent, TouchEventType, TouchId, + WebView, WindowRenderingContext, +}; +use url::Url; + +use crate::egl::host_trait::HostTrait; +use crate::prefs::ServoShellPreferences; +use crate::running_app_state::RunningAppState; +use crate::window::{PlatformWindow, ServoShellWindow, ServoShellWindowId}; + +pub(super) struct EmbeddedPlatformWindow { + host: Box, + rendering_context: Rc, + refresh_driver: Rc, + viewport_rect: RefCell>, + /// The HiDPI scaling factor to use for the display of [`WebView`]s. + hidpi_scale_factor: Scale, + /// A list of showing [`InputMethod`] interfaces. + visible_input_methods: RefCell>, + /// The current title of the focused WebView in this window. + current_title: RefCell>, + /// The current URL of the focused WebView in this window. + current_url: RefCell>, + /// Whether or not the focused WebView is currently able to go back. + current_can_go_back: Cell, + /// Whether or not the focused WebView is currently able to go forward. + current_can_go_forward: Cell, + /// The current load status of the focused WebView. + current_load_status: Cell>, +} + +impl PlatformWindow for EmbeddedPlatformWindow { + fn id(&self) -> ServoShellWindowId { + 0.into() + } + + fn screen_geometry(&self) -> ScreenGeometry { + let viewport_rect = self.viewport_rect.borrow(); + ScreenGeometry { + size: viewport_rect.size, + available_size: viewport_rect.size, + window_rect: viewport_rect.to_box2d(), + } + } + + fn device_hidpi_scale_factor(&self) -> Scale { + self.hidpi_scale_factor + } + + fn hidpi_scale_factor(&self) -> Scale { + self.hidpi_scale_factor + } + + fn get_fullscreen(&self) -> bool { + false + } + + fn rebuild_user_interface(&self, _: &RunningAppState, _: &ServoShellWindow) {} + + #[cfg_attr(target_os = "android", expect(unused_variables))] + fn update_user_interface_state( + &self, + state: &RunningAppState, + window: &ServoShellWindow, + ) -> bool { + let Some(focused_webview) = window.focused_webview() else { + return false; + }; + + let new_title = focused_webview.page_title(); + let title_changed = new_title != *self.current_title.borrow(); + if title_changed { + *self.current_title.borrow_mut() = new_title.clone(); + self.host.on_title_changed(new_title); + } + + let new_url = focused_webview.url(); + let url_changed = new_url != *self.current_url.borrow(); + if url_changed { + let new_url_string = new_url.as_ref().map(Url::to_string).unwrap_or_default(); + *self.current_url.borrow_mut() = new_url; + self.host.on_url_changed(new_url_string); + } + + let new_back_forward = ( + focused_webview.can_go_back(), + focused_webview.can_go_forward(), + ); + let old_back_forward = ( + self.current_can_go_back.get(), + self.current_can_go_forward.get(), + ); + let back_forward_changed = new_back_forward != old_back_forward; + if back_forward_changed { + self.current_can_go_back.set(new_back_forward.0); + self.current_can_go_forward.set(new_back_forward.1); + self.host + .on_history_changed(new_back_forward.0, new_back_forward.1); + } + + let new_load_status = focused_webview.load_status(); + let load_status_changed = Some(new_load_status) != self.current_load_status.get(); + if load_status_changed { + self.host.notify_load_status_changed(new_load_status); + + #[cfg(all(feature = "tracing", feature = "tracing-hitrace"))] + if new_load_status == LoadStatus::Complete { + let (sender, receiver) = + ipc_channel::ipc::channel().expect("Could not create channel"); + state.servo().create_memory_report(sender); + std::thread::spawn(move || { + let result = receiver.recv().expect("Could not get memory report"); + let reports = result + .results + .first() + .expect("We should have some memory report"); + for report in &reports.reports { + let path = String::from("servo_memory_profiling:") + &report.path.join("/"); + hitrace::trace_metric_str(&path, report.size as i64); + } + }); + } + } + + title_changed || url_changed || back_forward_changed || load_status_changed + } + + fn request_repaint(&self, window: &ServoShellWindow) { + window.repaint_webviews(); + } + + fn request_resize(&self, _: &WebView, _: DeviceIntSize) -> Option { + None + } + + fn rendering_context(&self) -> Rc { + self.rendering_context.clone() + } + + fn window_rect(&self) -> DeviceIndependentIntRect { + convert_rect_to_css_pixel( + self.viewport_rect.borrow().to_box2d(), + self.hidpi_scale_factor(), + ) + } + + fn focused(&self) -> bool { + true + } + + fn show_embedder_control(&self, _: WebViewId, embedder_control: EmbedderControl) { + let control_id = embedder_control.id(); + match embedder_control { + EmbedderControl::InputMethod(input_method_control) => { + self.visible_input_methods.borrow_mut().push(control_id); + self.host.on_ime_show(input_method_control); + }, + EmbedderControl::SimpleDialog(simple_dialog) => match simple_dialog { + SimpleDialog::Alert { + message, + response_sender, + .. + } => { + self.host.show_alert(message); + let _ = response_sender.send(AlertResponse::Ok); + }, + SimpleDialog::Confirm { + response_sender, .. + } => { + let _ = response_sender.send(Default::default()); + }, + SimpleDialog::Prompt { + response_sender, .. + } => { + let _ = response_sender.send(Default::default()); + }, + }, + _ => {}, + } + } + + fn hide_embedder_control(&self, _: WebViewId, control_id: servo::EmbedderControlId) { + let mut visible_input_methods = self.visible_input_methods.borrow_mut(); + if let Some(index) = visible_input_methods + .iter() + .position(|visible_id| *visible_id == control_id) + { + visible_input_methods.remove(index); + self.host.on_ime_hide(); + } + } + + fn notify_media_session_event(&self, event: MediaSessionEvent) { + match event { + MediaSessionEvent::SetMetadata(metadata) => { + self.host + .on_media_session_metadata(metadata.title, metadata.artist, metadata.album) + }, + MediaSessionEvent::PlaybackStateChange(state) => { + self.host.on_media_session_playback_state_change(state) + }, + MediaSessionEvent::SetPositionState(position_state) => { + self.host.on_media_session_set_position_state( + position_state.duration, + position_state.position, + position_state.playback_rate, + ) + }, + }; + } + + fn notify_crashed(&self, _webview: WebView, reason: String, backtrace: Option) { + self.host.on_panic(reason, backtrace); + } +} + +#[derive(Default)] +pub(crate) struct VsyncRefreshDriver { + start_frame_callbacks: RefCell>>, +} + +impl VsyncRefreshDriver { + fn notify_vsync(&self) { + let start_frame_callbacks: Vec<_> = + self.start_frame_callbacks.borrow_mut().drain(..).collect(); + for start_frame_callback in start_frame_callbacks { + start_frame_callback() + } + } +} + +impl RefreshDriver for VsyncRefreshDriver { + fn observe_next_frame(&self, new_start_frame_callback: Box) { + self.start_frame_callbacks + .borrow_mut() + .push(new_start_frame_callback); + } +} + +pub(crate) struct AppInitOptions { + pub host: Box, + pub event_loop_waker: Box, + pub viewport_rect: Rect, + pub hidpi_scale_factor: f32, + pub rendering_context: Rc, + pub refresh_driver: Rc, + pub initial_url: Option, + pub opts: Opts, + pub preferences: Preferences, + pub servoshell_preferences: ServoShellPreferences, + #[cfg(feature = "webxr")] + pub xr_discovery: Option, +} + +pub struct App { + state: Rc, + platform_window: Rc, +} + +#[allow(unused)] +impl App { + pub(super) fn new(init: AppInitOptions) -> Rc { + let mut servo_builder = ServoBuilder::default() + .opts(init.opts) + .preferences(init.preferences) + .event_loop_waker(init.event_loop_waker.clone()); + #[cfg(feature = "webxr")] + let servo_builder = servo_builder + .webxr_registry(Box::new(XrDiscoveryWebXrRegistry::new(init.xr_discovery))); + let servo = servo_builder.build(); + + let initial_url = init.initial_url.and_then(|string| Url::parse(&string).ok()); + let initial_url = initial_url + .or_else(|| Url::parse(&init.servoshell_preferences.homepage).ok()) + .or_else(|| Url::parse("about:blank").ok()) + .expect("Failed to parse initial URL"); + + let state = Rc::new(RunningAppState::new( + servo, + init.servoshell_preferences, + init.event_loop_waker, + )); + + let platform_window = Rc::new(EmbeddedPlatformWindow { + host: init.host, + rendering_context: init.rendering_context, + refresh_driver: init.refresh_driver, + viewport_rect: RefCell::new(init.viewport_rect), + hidpi_scale_factor: Scale::new(init.hidpi_scale_factor), + visible_input_methods: Default::default(), + current_title: Default::default(), + current_url: Default::default(), + current_can_go_back: Default::default(), + current_can_go_forward: Default::default(), + current_load_status: Default::default(), + }); + state.create_window(platform_window.clone(), initial_url); + + Rc::new(Self { + state, + platform_window, + }) + } + + pub(crate) fn servo(&self) -> &Servo { + &self.state.servo + } + + pub(crate) fn servoshell_preferences(&self) -> &ServoShellPreferences { + &self.state.servoshell_preferences + } + + pub(crate) fn focused_or_newest_webview(&self) -> Option { + self.state.any_window().focused_or_newest_webview() + } + + pub(crate) fn create_and_focus_toplevel_webview(self: &Rc, url: Url) -> WebView { + self.state + .any_window() + .create_and_focus_toplevel_webview(self.state.clone(), url) + } + + /// The focused webview will not be immediately valid via `focused_or_newest_webview()` + pub(crate) fn focus_webview(&self, id: WebViewId) { + if let Some(webview) = self.state.webview_by_id(id) { + webview.focus(); + } + } + + /// Request shutdown. Will call on_shutdown_complete. + pub fn request_shutdown(&self) { + self.state.servo.start_shutting_down(); + self.spin_event_loop(); + } + + /// This is the Servo heartbeat. This needs to be called + /// everytime wakeup is called or when embedder wants Servo + /// to act on its pending events. + pub fn spin_event_loop(&self) { + if !self.state.spin_event_loop() { + self.platform_window.host.on_shutdown_complete(); + } + } + + /// Load an URL. + pub fn load_uri(&self, url: &str) { + if let Some(webview) = self.focused_or_newest_webview() { + let Some(url) = crate::parser::location_bar_input_to_url( + url, + &self.servoshell_preferences().searchpage, + ) else { + warn!("Cannot parse URL"); + return; + }; + webview.load(url.into_url()); + } + } + + /// Reload the page. + pub fn reload(&self) { + if let Some(webview) = self.focused_or_newest_webview() { + webview.reload(); + self.spin_event_loop(); + } + } + + /// Stop loading the page. + pub fn stop(&self) { + warn!("TODO can't stop won't stop"); + } + + /// Go back in history. + pub fn go_back(&self) { + if let Some(webview) = self.focused_or_newest_webview() { + webview.go_back(1); + self.spin_event_loop(); + } + } + + /// Go forward in history. + pub fn go_forward(&self) { + if let Some(webview) = self.focused_or_newest_webview() { + webview.go_forward(1); + self.spin_event_loop(); + } + } + + /// Let Servo know that the window has been resized. + pub fn resize(&self, viewport_rect: Rect) { + if let Some(webview) = self.focused_or_newest_webview() { + info!("Setting viewport to {viewport_rect:?}"); + let size = viewport_rect.size; + webview.move_resize(size.to_f32().into()); + webview.resize(PhysicalSize::new(size.width as u32, size.height as u32)); + } + *self.platform_window.viewport_rect.borrow_mut() = viewport_rect; + self.spin_event_loop(); + } + + /// Scroll. + /// x/y are scroll coordinates. + /// dx/dy are scroll deltas. + pub fn scroll(&self, dx: f32, dy: f32, x: f32, y: f32) { + if let Some(webview) = self.focused_or_newest_webview() { + let scroll = Scroll::Delta(DeviceVector2D::new(dx, dy).into()); + let point = DevicePoint::new(x, y).into(); + webview.notify_scroll_event(scroll, point); + self.spin_event_loop(); + } + } + + /// Touch event: press down + pub fn touch_down(&self, x: f32, y: f32, pointer_id: i32) { + if let Some(webview) = self.focused_or_newest_webview() { + webview.notify_input_event(InputEvent::Touch(TouchEvent::new( + TouchEventType::Down, + TouchId(pointer_id), + DevicePoint::new(x, y).into(), + ))); + self.spin_event_loop(); + } + } + + /// Touch event: move touching finger + pub fn touch_move(&self, x: f32, y: f32, pointer_id: i32) { + if let Some(webview) = self.focused_or_newest_webview() { + webview.notify_input_event(InputEvent::Touch(TouchEvent::new( + TouchEventType::Move, + TouchId(pointer_id), + DevicePoint::new(x, y).into(), + ))); + self.spin_event_loop(); + } + } + + /// Touch event: Lift touching finger + pub fn touch_up(&self, x: f32, y: f32, pointer_id: i32) { + if let Some(webview) = self.focused_or_newest_webview() { + webview.notify_input_event(InputEvent::Touch(TouchEvent::new( + TouchEventType::Up, + TouchId(pointer_id), + DevicePoint::new(x, y).into(), + ))); + self.spin_event_loop(); + } + } + + /// Cancel touch event + pub fn touch_cancel(&self, x: f32, y: f32, pointer_id: i32) { + if let Some(webview) = self.focused_or_newest_webview() { + webview.notify_input_event(InputEvent::Touch(TouchEvent::new( + TouchEventType::Cancel, + TouchId(pointer_id), + DevicePoint::new(x, y).into(), + ))); + self.spin_event_loop(); + } + } + + /// Register a mouse movement. + pub fn mouse_move(&self, x: f32, y: f32) { + if let Some(webview) = self.focused_or_newest_webview() { + webview.notify_input_event(InputEvent::MouseMove(MouseMoveEvent::new( + DevicePoint::new(x, y).into(), + ))); + self.spin_event_loop(); + } + } + + /// Register a mouse button press. + pub fn mouse_down(&self, x: f32, y: f32, button: MouseButton) { + if let Some(webview) = self.focused_or_newest_webview() { + webview.notify_input_event(InputEvent::MouseButton(MouseButtonEvent::new( + MouseButtonAction::Down, + button, + DevicePoint::new(x, y).into(), + ))); + self.spin_event_loop(); + } + } + + /// Register a mouse button release. + pub fn mouse_up(&self, x: f32, y: f32, button: MouseButton) { + if let Some(webview) = self.focused_or_newest_webview() { + webview.notify_input_event(InputEvent::MouseButton(MouseButtonEvent::new( + MouseButtonAction::Up, + button, + DevicePoint::new(x, y).into(), + ))); + self.spin_event_loop(); + } + } + + /// Start pinchzoom. + /// x/y are pinch origin coordinates. + pub fn pinchzoom_start(&self, factor: f32, x: f32, y: f32) { + if let Some(webview) = self.focused_or_newest_webview() { + webview.pinch_zoom(factor, DevicePoint::new(x, y)); + self.spin_event_loop(); + } + } + + /// Pinchzoom. + /// x/y are pinch origin coordinates. + pub fn pinchzoom(&self, factor: f32, x: f32, y: f32) { + if let Some(webview) = self.focused_or_newest_webview() { + webview.pinch_zoom(factor, DevicePoint::new(x, y)); + self.spin_event_loop(); + } + } + + /// End pinchzoom. + /// x/y are pinch origin coordinates. + pub fn pinchzoom_end(&self, factor: f32, x: f32, y: f32) { + if let Some(webview) = self.focused_or_newest_webview() { + webview.pinch_zoom(factor, DevicePoint::new(x, y)); + self.spin_event_loop(); + } + } + + pub fn key_down(&self, key: Key) { + if let Some(webview) = self.focused_or_newest_webview() { + webview.notify_input_event(InputEvent::Keyboard(KeyboardEvent::from_state_and_key( + KeyState::Down, + key, + ))); + self.spin_event_loop(); + } + } + + pub fn key_up(&self, key: Key) { + if let Some(webview) = self.focused_or_newest_webview() { + webview.notify_input_event(InputEvent::Keyboard(KeyboardEvent::from_state_and_key( + KeyState::Up, + key, + ))); + self.spin_event_loop(); + } + } + + pub fn ime_insert_text(&self, text: String) { + // In OHOS, we get empty text after the intended text. + if text.is_empty() { + return; + } + + let Some(webview) = self.focused_or_newest_webview() else { + return; + }; + + webview.notify_input_event(InputEvent::Keyboard(KeyboardEvent::from_state_and_key( + KeyState::Down, + Key::Named(NamedKey::Process), + ))); + webview.notify_input_event(InputEvent::Ime(ImeEvent::Composition(CompositionEvent { + state: CompositionState::End, + data: text, + }))); + webview.notify_input_event(InputEvent::Keyboard(KeyboardEvent::from_state_and_key( + KeyState::Up, + Key::Named(NamedKey::Process), + ))); + self.spin_event_loop(); + } + + pub fn media_session_action(&self, action: MediaSessionActionType) { + if let Some(webview) = self.focused_or_newest_webview() { + webview.notify_media_session_action_event(action); + self.spin_event_loop(); + } + } + + pub fn set_throttled(&self, throttled: bool) { + if let Some(webview) = self.focused_or_newest_webview() { + webview.set_throttled(throttled); + self.spin_event_loop(); + } + } + + pub fn ime_dismissed(&self) { + if let Some(webview) = self.focused_or_newest_webview() { + webview.notify_input_event(InputEvent::Ime(ImeEvent::Dismissed)); + self.spin_event_loop(); + } + } + + pub fn notify_vsync(&self) { + self.platform_window.refresh_driver.notify_vsync(); + self.spin_event_loop(); + } + + pub fn pause_compositor(&self) { + if let Err(e) = self.platform_window.rendering_context.take_window() { + warn!("Unbinding native surface from context failed ({:?})", e); + } + self.spin_event_loop(); + } + + pub fn resume_compositor( + &self, + window_handle: RawWindowHandle, + viewport_rect: Rect, + ) { + let window_handle = unsafe { WindowHandle::borrow_raw(window_handle) }; + let size = viewport_rect.size.to_u32(); + if let Err(error) = self + .platform_window + .rendering_context + .set_window(window_handle, PhysicalSize::new(size.width, size.height)) + { + warn!("Binding native surface to context failed ({error:?})"); + } + self.spin_event_loop(); + } +} + +#[cfg(feature = "webxr")] +pub(crate) struct XrDiscoveryWebXrRegistry { + xr_discovery: RefCell>, +} + +#[cfg(feature = "webxr")] +#[cfg_attr(target_env = "ohos", allow(dead_code))] +impl XrDiscoveryWebXrRegistry { + pub(crate) fn new(xr_discovery: Option) -> Self { + Self { + xr_discovery: RefCell::new(xr_discovery), + } + } +} + +#[cfg(feature = "webxr")] +impl servo::webxr::WebXrRegistry for XrDiscoveryWebXrRegistry { + fn register(&self, registry: &mut servo::webxr::MainThreadRegistry) { + log::debug!("XrDiscoveryWebXrRegistry::register"); + if let Some(discovery) = self.xr_discovery.take() { + registry.register(discovery); + } + } +} diff --git a/ports/servoshell/egl/app_state.rs b/ports/servoshell/egl/app_state.rs deleted file mode 100644 index 1e4b1919f8e..00000000000 --- a/ports/servoshell/egl/app_state.rs +++ /dev/null @@ -1,833 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use std::cell::{Ref, RefCell, RefMut}; -use std::rc::Rc; - -use crossbeam_channel::Receiver; -use dpi::PhysicalSize; -use euclid::{Point2D, Rect, Scale, Size2D}; -use keyboard_types::{CompositionEvent, CompositionState, Key, KeyState, NamedKey}; -use log::{debug, error, info, warn}; -use raw_window_handle::{RawWindowHandle, WindowHandle}; -use servo::base::id::WebViewId; -use servo::servo_geometry::DeviceIndependentPixel; -use servo::webrender_api::units::{ - DeviceIntRect, DeviceIntSize, DevicePixel, DevicePoint, DeviceVector2D, -}; -use servo::{ - EmbedderControl, EmbedderControlId, ImeEvent, InputEvent, InputEventId, InputEventResult, - KeyboardEvent, LoadStatus, MediaSessionActionType, MediaSessionEvent, MouseButton, - MouseButtonAction, MouseButtonEvent, MouseMoveEvent, NavigationRequest, PermissionRequest, - RefreshDriver, RenderingContext, ScreenGeometry, Scroll, Servo, TouchEvent, TouchEventType, - TouchId, TraversalId, WebDriverCommandMsg, WebDriverLoadStatus, WebView, WebViewBuilder, - WebViewDelegate, WindowRenderingContext, -}; -use url::Url; - -use crate::egl::host_trait::HostTrait; -use crate::prefs::ServoShellPreferences; -use crate::running_app_state::{RunningAppStateBase, RunningAppStateTrait}; - -#[derive(Clone, Debug)] -pub struct Coordinates { - pub viewport: Rect, -} - -impl Coordinates { - pub fn new(x: i32, y: i32, width: i32, height: i32) -> Coordinates { - Coordinates { - viewport: Rect::new(Point2D::new(x, y), Size2D::new(width, height)), - } - } - - pub fn origin(&self) -> Point2D { - self.viewport.origin - } - - pub fn size(&self) -> Size2D { - self.viewport.size - } -} - -pub(super) struct ServoWindowCallbacks { - host_callbacks: Box, - coordinates: RefCell, -} - -impl ServoWindowCallbacks { - pub(super) fn new( - host_callbacks: Box, - coordinates: RefCell, - ) -> Self { - Self { - host_callbacks, - coordinates, - } - } -} - -pub struct RunningAppState { - base: RunningAppStateBase, - rendering_context: Rc, - callbacks: Rc, - refresh_driver: Option>, - inner: RefCell, -} - -struct RunningAppStateInner { - need_present: bool, - - /// The HiDPI scaling factor to use for the display of [`WebView`]s. - hidpi_scale_factor: Scale, - - /// A list of showing [`InputMethod`] interfaces. - visible_input_methods: Vec, -} - -impl WebViewDelegate for RunningAppState { - fn screen_geometry(&self, _webview: WebView) -> Option { - let coord = self.callbacks.coordinates.borrow(); - let available_size = coord.size(); - let screen_size = coord.size(); - Some(ScreenGeometry { - size: screen_size, - available_size, - window_rect: DeviceIntRect::from_origin_and_size(coord.origin(), coord.size()), - }) - } - - fn notify_page_title_changed(&self, _webview: servo::WebView, title: Option) { - self.callbacks.host_callbacks.on_title_changed(title); - } - - fn notify_history_changed(&self, _webview: WebView, entries: Vec, current: usize) { - let can_go_back = current > 0; - let can_go_forward = current < entries.len() - 1; - self.callbacks - .host_callbacks - .on_history_changed(can_go_back, can_go_forward); - self.callbacks - .host_callbacks - .on_url_changed(entries[current].clone().to_string()); - } - - fn notify_load_status_changed(&self, webview: WebView, load_status: LoadStatus) { - self.callbacks - .host_callbacks - .notify_load_status_changed(load_status); - - if load_status == LoadStatus::Complete { - if let Some(sender) = self - .base() - .webdriver_senders - .borrow_mut() - .load_status_senders - .remove(&webview.id()) - { - let _ = sender.send(WebDriverLoadStatus::Complete); - } - self.maybe_request_screenshot(webview); - } - - #[cfg(feature = "tracing")] - if load_status == LoadStatus::Complete { - #[cfg(feature = "tracing-hitrace")] - let (snd, recv) = ipc_channel::ipc::channel().expect("Could not create channel"); - self.servo().create_memory_report(snd); - std::thread::spawn(move || { - let result = recv.recv().expect("Could not get memory report"); - let reports = result - .results - .first() - .expect("We should have some memory report"); - for report in &reports.reports { - let path = String::from("servo_memory_profiling:") + &report.path.join("/"); - hitrace::trace_metric_str(&path, report.size as i64); - } - }); - } - } - - fn notify_closed(&self, webview: WebView) { - self.webview_collection_mut().remove(webview.id()); - - if let Some(newest_webview) = self.webview_collection().newest() { - newest_webview.focus(); - } else { - self.servo().start_shutting_down(); - } - } - - fn notify_focus_changed(&self, webview: WebView, focused: bool) { - if focused { - self.webview_collection_mut() - .set_focused(Some(webview.id())); - webview.show(true); - } else if self.webview_collection().focused_id() == Some(webview.id()) { - self.webview_collection_mut().set_focused(None); - } - } - - fn notify_traversal_complete(&self, _webview: servo::WebView, traversal_id: TraversalId) { - let mut webdriver_state = self.base().webdriver_senders.borrow_mut(); - if let std::collections::hash_map::Entry::Occupied(entry) = - webdriver_state.pending_traversals.entry(traversal_id) - { - let sender = entry.remove(); - let _ = sender.send(WebDriverLoadStatus::Complete); - } - } - - fn notify_media_session_event(&self, _webview: WebView, event: MediaSessionEvent) { - match event { - MediaSessionEvent::SetMetadata(metadata) => self - .callbacks - .host_callbacks - .on_media_session_metadata(metadata.title, metadata.artist, metadata.album), - MediaSessionEvent::PlaybackStateChange(state) => self - .callbacks - .host_callbacks - .on_media_session_playback_state_change(state), - MediaSessionEvent::SetPositionState(position_state) => self - .callbacks - .host_callbacks - .on_media_session_set_position_state( - position_state.duration, - position_state.position, - position_state.playback_rate, - ), - }; - } - - fn notify_crashed(&self, _webview: WebView, reason: String, backtrace: Option) { - self.callbacks.host_callbacks.on_panic(reason, backtrace); - } - - fn notify_new_frame_ready(&self, _webview: WebView) { - self.inner_mut().need_present = true; - } - - fn notify_input_event_handled( - &self, - _webview: WebView, - id: InputEventId, - _result: InputEventResult, - ) { - if let Some(response_sender) = self - .base() - .pending_webdriver_events - .borrow_mut() - .remove(&id) - { - let _ = response_sender.send(()); - } - } - - fn request_navigation(&self, _webview: WebView, navigation_request: NavigationRequest) { - if self - .callbacks - .host_callbacks - .on_allow_navigation(navigation_request.url.to_string()) - { - navigation_request.allow(); - } else { - navigation_request.deny(); - } - } - - fn request_open_auxiliary_webview(&self, parent_webview: WebView) -> Option { - let webview = WebViewBuilder::new_auxiliary(self.servo(), self.rendering_context.clone()) - .delegate(parent_webview.delegate()) - .hidpi_scale_factor(self.inner().hidpi_scale_factor) - .build(); - self.add_webview(webview.clone()); - Some(webview) - } - - fn request_permission(&self, webview: WebView, request: PermissionRequest) { - self.callbacks - .host_callbacks - .request_permission(webview, request); - } - - fn request_resize_to(&self, _webview: WebView, size: DeviceIntSize) { - warn!("Received resize event (to {size:?}). Currently only the user can resize windows"); - } - - fn show_embedder_control(&self, webview: WebView, embedder_control: EmbedderControl) { - let control_id = embedder_control.id(); - match embedder_control { - EmbedderControl::InputMethod(input_method_control) => { - self.inner_mut().visible_input_methods.push(control_id); - self.callbacks - .host_callbacks - .on_ime_show(input_method_control); - }, - EmbedderControl::SimpleDialog(simple_dialog) => self - .callbacks - .host_callbacks - .show_simple_dialog(webview, simple_dialog), - _ => {}, - } - } - - fn hide_embedder_control(&self, _webview: WebView, control_id: servo::EmbedderControlId) { - let mut inner_mut = self.inner_mut(); - if let Some(index) = inner_mut - .visible_input_methods - .iter() - .position(|visible_id| *visible_id == control_id) - { - inner_mut.visible_input_methods.remove(index); - self.callbacks.host_callbacks.on_ime_hide(); - } - } -} - -#[derive(Default)] -pub(crate) struct VsyncRefreshDriver { - start_frame_callbacks: RefCell>>, -} - -impl VsyncRefreshDriver { - fn notify_vsync(&self) { - let start_frame_callbacks: Vec<_> = - self.start_frame_callbacks.borrow_mut().drain(..).collect(); - for start_frame_callback in start_frame_callbacks { - start_frame_callback() - } - } -} - -impl RefreshDriver for VsyncRefreshDriver { - fn observe_next_frame(&self, new_start_frame_callback: Box) { - self.start_frame_callbacks - .borrow_mut() - .push(new_start_frame_callback); - } -} - -impl RunningAppStateTrait for RunningAppState { - fn base(&self) -> &RunningAppStateBase { - &self.base - } - - fn base_mut(&mut self) -> &mut RunningAppStateBase { - &mut self.base - } - - fn webview_by_id(&self, id: WebViewId) -> Option { - self.webview_collection().get(id).cloned() - } -} - -#[allow(unused)] -impl RunningAppState { - pub(super) fn new( - initial_url: Option, - hidpi_scale_factor: f32, - rendering_context: Rc, - servo: Servo, - callbacks: Rc, - refresh_driver: Option>, - servoshell_preferences: ServoShellPreferences, - webdriver_receiver: Option>, - ) -> Rc { - let initial_url = initial_url.and_then(|string| Url::parse(&string).ok()); - let initial_url = initial_url - .or_else(|| Url::parse(&servoshell_preferences.homepage).ok()) - .or_else(|| Url::parse("about:blank").ok()) - .unwrap(); - - let app_state = Rc::new(Self { - base: RunningAppStateBase::new(servoshell_preferences, servo, webdriver_receiver), - rendering_context, - callbacks, - refresh_driver, - inner: RefCell::new(RunningAppStateInner { - need_present: false, - hidpi_scale_factor: Scale::new(hidpi_scale_factor), - visible_input_methods: Default::default(), - }), - }); - - app_state.create_and_focus_toplevel_webview(initial_url); - app_state - } - - pub(crate) fn create_and_focus_toplevel_webview(self: &Rc, url: Url) -> WebView { - let webview = WebViewBuilder::new(self.servo(), self.rendering_context.clone()) - .url(url) - .hidpi_scale_factor(self.inner().hidpi_scale_factor) - .delegate(self.clone()) - .build(); - - webview.focus(); - self.add_webview(webview.clone()); - webview - } - - /// The focused webview will not be immediately valid via `active_webview()` - pub(crate) fn focus_webview(&self, id: WebViewId) { - if let Some(webview) = self.webview_collection().get(id) { - webview.focus(); - } else { - error!("We could not find the webview with this id {id}"); - } - } - - fn inner(&self) -> Ref<'_, RunningAppStateInner> { - self.inner.borrow() - } - - fn inner_mut(&self) -> RefMut<'_, RunningAppStateInner> { - self.inner.borrow_mut() - } - - fn get_browser_id(&self) -> Result { - self.webview_collection() - .focused_id() - .ok_or("No focused WebViewId yet.") - } - - /// Request shutdown. Will call on_shutdown_complete. - pub fn request_shutdown(&self) { - self.servo().start_shutting_down(); - self.perform_updates(); - } - - /// Call after on_shutdown_complete - pub fn deinit(self) { - self.servo().deinit(); - } - - /// This is the Servo heartbeat. This needs to be called - /// everytime wakeup is called or when embedder wants Servo - /// to act on its pending events. - pub fn perform_updates(&self) { - let should_continue = self.servo().spin_event_loop(); - if !should_continue { - self.callbacks.host_callbacks.on_shutdown_complete(); - } - } - - /// Load an URL. - pub fn load_uri(&self, url: &str) { - info!("load_uri: {}", url); - - let Some(url) = crate::parser::location_bar_input_to_url( - url, - &self.servoshell_preferences().searchpage, - ) else { - warn!("Cannot parse URL"); - return; - }; - - self.active_webview_or_panic().load(url.into_url()); - } - - /// Reload the page. - pub fn reload(&self) { - info!("reload"); - self.active_webview_or_panic().reload(); - self.perform_updates(); - } - - /// Stop loading the page. - pub fn stop(&self) { - warn!("TODO can't stop won't stop"); - } - - /// Go back in history. - pub fn go_back(&self) { - info!("go_back"); - self.active_webview_or_panic().go_back(1); - self.perform_updates(); - } - - /// Go forward in history. - pub fn go_forward(&self) { - info!("go_forward"); - self.active_webview_or_panic().go_forward(1); - self.perform_updates(); - } - - /// Let Servo know that the window has been resized. - pub fn resize(&self, coordinates: Coordinates) { - info!("resize to {:?}", coordinates,); - let size = coordinates.viewport.size; - self.active_webview_or_panic() - .move_resize(size.to_f32().into()); - self.active_webview_or_panic() - .resize(PhysicalSize::new(size.width as u32, size.height as u32)); - *self.callbacks.coordinates.borrow_mut() = coordinates; - self.perform_updates(); - } - - /// Scroll. - /// x/y are scroll coordinates. - /// dx/dy are scroll deltas. - pub fn scroll(&self, dx: f32, dy: f32, x: f32, y: f32) { - let scroll = Scroll::Delta(DeviceVector2D::new(dx, dy).into()); - let point = DevicePoint::new(x, y).into(); - self.active_webview_or_panic() - .notify_scroll_event(scroll, point); - self.perform_updates(); - } - - /// WebDriver message handling methods - pub(crate) fn handle_webdriver_messages(self: &Rc) { - if let Some(webdriver_receiver) = &self.webdriver_receiver() { - while let Ok(msg) = webdriver_receiver.try_recv() { - match msg { - WebDriverCommandMsg::LoadUrl(webview_id, url, load_status_sender) => { - self.handle_webdriver_load_url(webview_id, url, load_status_sender); - }, - WebDriverCommandMsg::NewWebView(response_sender, load_status_sender) => { - info!("Creating new webview via WebDriver"); - let new_webview = self - .create_and_focus_toplevel_webview(Url::parse("about:blank").unwrap()); - - if let Err(error) = response_sender.send(new_webview.id()) { - warn!("Failed to send response of NewWebview: {error}"); - } - if let Some(load_status_sender) = load_status_sender { - self.set_load_status_sender(new_webview.id(), load_status_sender); - } - }, - WebDriverCommandMsg::CloseWebView(webview_id, response_sender) => { - info!("(Not Implemented) Closing webview {}", webview_id); - }, - WebDriverCommandMsg::FocusWebView(webview_id) => { - if let Some(webview) = self.webview_by_id(webview_id) { - let focus_id = webview.focus(); - info!("Successfully focused webview {}", webview_id); - } - }, - WebDriverCommandMsg::IsWebViewOpen(webview_id, response_sender) => { - let context = self.webview_by_id(webview_id); - - if let Err(error) = response_sender.send(context.is_some()) { - warn!("Failed to send response of IsWebViewOpen: {error}"); - } - }, - WebDriverCommandMsg::IsBrowsingContextOpen(..) => { - self.servo().execute_webdriver_command(msg); - }, - WebDriverCommandMsg::GetFocusedWebView(response_sender) => { - let focused_id = self.webview_collection().focused_id(); - - if let Err(error) = response_sender.send(focused_id) { - warn!("Failed to send response of GetFocusedWebView: {error}"); - } - }, - WebDriverCommandMsg::Refresh(webview_id, load_status_sender) => { - info!("Refreshing webview {}", webview_id); - if let Some(webview) = self.webview_by_id(webview_id) { - self.set_load_status_sender(webview_id, load_status_sender); - webview.reload(); - } else { - warn!("WebView {} not found for Refresh command", webview_id); - } - }, - WebDriverCommandMsg::GoBack(webview_id, load_status_sender) => { - info!("Going back in webview {}", webview_id); - if let Some(webview) = self.webview_by_id(webview_id) { - let traversal_id = webview.go_back(1); - self.set_pending_traversal(traversal_id, load_status_sender); - } else { - warn!("WebView {} not found for GoBack command", webview_id); - } - }, - WebDriverCommandMsg::GoForward(webview_id, load_status_sender) => { - info!("Going forward in webview {}", webview_id); - if let Some(webview) = self.webview_by_id(webview_id) { - let traversal_id = webview.go_forward(1); - self.set_pending_traversal(traversal_id, load_status_sender); - } else { - warn!("WebView {} not found for GoForward command", webview_id); - } - }, - WebDriverCommandMsg::GetAllWebViews(response_sender) => { - let webviews = self - .webviews() - .iter() - .map(|(id, _)| *id) - .collect::>(); - - if let Err(error) = response_sender.send(webviews) { - warn!("Failed to send response of GetAllWebViews: {error}"); - } - }, - WebDriverCommandMsg::ScriptCommand(_, ref webdriver_script_command) => { - self.handle_webdriver_script_command(webdriver_script_command); - self.servo().execute_webdriver_command(msg); - }, - WebDriverCommandMsg::CurrentUserPrompt(webview_id, response_sender) => { - info!("Handling CurrentUserPrompt for webview {}", webview_id); - if let Err(error) = response_sender.send(None) { - warn!("Failed to send response of CurrentUserPrompt: {error}"); - }; - }, - WebDriverCommandMsg::HandleUserPrompt(webview_id, action, response_sender) => { - info!( - "Handling HandleUserPrompt for webview {} with action {:?}", - webview_id, action - ); - - if let Err(error) = response_sender.send(Err(())) { - warn!("Failed to send response of HandleUserPrompt: {error}"); - }; - }, - WebDriverCommandMsg::GetAlertText(webview_id, response_sender) => { - info!("Handling GetAlertText for webview {}", webview_id); - let _ = response_sender.send(Err(())); - }, - WebDriverCommandMsg::SendAlertText(webview_id, text) => { - info!( - "Handling SendAlertText for webview {} with text: {}", - webview_id, text - ); - }, - WebDriverCommandMsg::GetViewportSize(webview_id, response_sender) => { - info!("Handling GetViewportSize for webview {}", webview_id); - let _ = response_sender.send(self.rendering_context.size2d()); - }, - WebDriverCommandMsg::InputEvent(webview_id, input_event, response_sender) => { - self.handle_webdriver_input_event(webview_id, input_event, response_sender); - }, - WebDriverCommandMsg::TakeScreenshot(webview_id, rect, result_sender) => { - self.handle_webdriver_screenshot(webview_id, rect, result_sender); - }, - _ => { - info!("Received unsupported WebDriver command: {:?}", msg); - }, - } - } - } - } - - /// Touch event: press down - pub fn touch_down(&self, x: f32, y: f32, pointer_id: i32) { - self.active_webview_or_panic() - .notify_input_event(InputEvent::Touch(TouchEvent::new( - TouchEventType::Down, - TouchId(pointer_id), - DevicePoint::new(x, y).into(), - ))); - self.perform_updates(); - } - - /// Touch event: move touching finger - pub fn touch_move(&self, x: f32, y: f32, pointer_id: i32) { - self.active_webview_or_panic() - .notify_input_event(InputEvent::Touch(TouchEvent::new( - TouchEventType::Move, - TouchId(pointer_id), - DevicePoint::new(x, y).into(), - ))); - self.perform_updates(); - } - - /// Touch event: Lift touching finger - pub fn touch_up(&self, x: f32, y: f32, pointer_id: i32) { - self.active_webview_or_panic() - .notify_input_event(InputEvent::Touch(TouchEvent::new( - TouchEventType::Up, - TouchId(pointer_id), - DevicePoint::new(x, y).into(), - ))); - self.perform_updates(); - } - - /// Cancel touch event - pub fn touch_cancel(&self, x: f32, y: f32, pointer_id: i32) { - self.active_webview_or_panic() - .notify_input_event(InputEvent::Touch(TouchEvent::new( - TouchEventType::Cancel, - TouchId(pointer_id), - DevicePoint::new(x, y).into(), - ))); - self.perform_updates(); - } - - /// Register a mouse movement. - pub fn mouse_move(&self, x: f32, y: f32) { - self.active_webview_or_panic() - .notify_input_event(InputEvent::MouseMove(MouseMoveEvent::new( - DevicePoint::new(x, y).into(), - ))); - self.perform_updates(); - } - - /// Register a mouse button press. - pub fn mouse_down(&self, x: f32, y: f32, button: MouseButton) { - self.active_webview_or_panic() - .notify_input_event(InputEvent::MouseButton(MouseButtonEvent::new( - MouseButtonAction::Down, - button, - DevicePoint::new(x, y).into(), - ))); - self.perform_updates(); - } - - /// Register a mouse button release. - pub fn mouse_up(&self, x: f32, y: f32, button: MouseButton) { - self.active_webview_or_panic() - .notify_input_event(InputEvent::MouseButton(MouseButtonEvent::new( - MouseButtonAction::Up, - button, - DevicePoint::new(x, y).into(), - ))); - self.perform_updates(); - } - - /// Start pinchzoom. - /// x/y are pinch origin coordinates. - pub fn pinchzoom_start(&self, factor: f32, x: f32, y: f32) { - self.active_webview_or_panic() - .pinch_zoom(factor, DevicePoint::new(x, y)); - self.perform_updates(); - } - - /// Pinchzoom. - /// x/y are pinch origin coordinates. - pub fn pinchzoom(&self, factor: f32, x: f32, y: f32) { - self.active_webview_or_panic() - .pinch_zoom(factor, DevicePoint::new(x, y)); - self.perform_updates(); - } - - /// End pinchzoom. - /// x/y are pinch origin coordinates. - pub fn pinchzoom_end(&self, factor: f32, x: f32, y: f32) { - self.active_webview_or_panic() - .pinch_zoom(factor, DevicePoint::new(x, y)); - self.perform_updates(); - } - - pub fn key_down(&self, key: Key) { - let key_event = KeyboardEvent::from_state_and_key(KeyState::Down, key); - self.active_webview_or_panic() - .notify_input_event(InputEvent::Keyboard(key_event)); - self.perform_updates(); - } - - pub fn key_up(&self, key: Key) { - let key_event = KeyboardEvent::from_state_and_key(KeyState::Up, key); - self.active_webview_or_panic() - .notify_input_event(InputEvent::Keyboard(key_event)); - self.perform_updates(); - } - - pub fn ime_insert_text(&self, text: String) { - // In OHOS, we get empty text after the intended text. - if text.is_empty() { - return; - } - let active_webview = self.active_webview_or_panic(); - active_webview.notify_input_event(InputEvent::Keyboard(KeyboardEvent::from_state_and_key( - KeyState::Down, - Key::Named(NamedKey::Process), - ))); - active_webview.notify_input_event(InputEvent::Ime(ImeEvent::Composition( - CompositionEvent { - state: CompositionState::End, - data: text, - }, - ))); - active_webview.notify_input_event(InputEvent::Keyboard(KeyboardEvent::from_state_and_key( - KeyState::Up, - Key::Named(NamedKey::Process), - ))); - self.perform_updates(); - } - - pub fn notify_vsync(&self) { - if let Some(refresh_driver) = &self.refresh_driver { - refresh_driver.notify_vsync(); - }; - self.perform_updates(); - } - - pub fn pause_compositor(&self) { - if let Err(e) = self.rendering_context.take_window() { - warn!("Unbinding native surface from context failed ({:?})", e); - } - self.perform_updates(); - } - - pub fn resume_compositor(&self, window_handle: RawWindowHandle, coords: Coordinates) { - let window_handle = unsafe { WindowHandle::borrow_raw(window_handle) }; - let size = coords.viewport.size.to_u32(); - if let Err(e) = self - .rendering_context - .set_window(window_handle, PhysicalSize::new(size.width, size.height)) - { - warn!("Binding native surface to context failed ({:?})", e); - } - self.perform_updates(); - } - - pub fn media_session_action(&self, action: MediaSessionActionType) { - info!("Media session action {:?}", action); - self.active_webview_or_panic() - .notify_media_session_action_event(action); - self.perform_updates(); - } - - pub fn set_throttled(&self, throttled: bool) { - info!("set_throttled"); - self.active_webview_or_panic().set_throttled(throttled); - self.perform_updates(); - } - - pub fn ime_dismissed(&self) { - info!("ime_dismissed"); - self.active_webview_or_panic() - .notify_input_event(InputEvent::Ime(ImeEvent::Dismissed)); - self.perform_updates(); - } - - pub fn present_if_needed(&self) { - if !self.inner().need_present { - return; - } - - self.inner_mut().need_present = false; - self.active_webview_or_panic().paint(); - self.rendering_context.present(); - - if self.servoshell_preferences().exit_after_stable_image && - self.base().achieved_stable_image.get() - { - self.request_shutdown(); - } - } -} - -#[cfg(feature = "webxr")] -pub(crate) struct XrDiscoveryWebXrRegistry { - xr_discovery: RefCell>, -} - -#[cfg(feature = "webxr")] -#[cfg_attr(target_env = "ohos", allow(dead_code))] -impl XrDiscoveryWebXrRegistry { - pub(crate) fn new(xr_discovery: Option) -> Self { - Self { - xr_discovery: RefCell::new(xr_discovery), - } - } -} - -#[cfg(feature = "webxr")] -impl servo::webxr::WebXrRegistry for XrDiscoveryWebXrRegistry { - fn register(&self, registry: &mut servo::webxr::MainThreadRegistry) { - debug!("XrDiscoveryWebXrRegistry::register"); - if let Some(discovery) = self.xr_discovery.take() { - registry.register(discovery); - } - } -} diff --git a/ports/servoshell/egl/gamepad.rs b/ports/servoshell/egl/gamepad.rs new file mode 100644 index 00000000000..2e341001e04 --- /dev/null +++ b/ports/servoshell/egl/gamepad.rs @@ -0,0 +1,34 @@ +/* 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 servo::ipc_channel::ipc::IpcSender; +use servo::{GamepadHapticEffectType, WebView}; + +/// A dummy version of [`crate::desktop::GamepadSupport`] used to avoid conditional compilation in +/// servoshell and as a skeleton to implement gamepad support for platforms that do not +/// currently support it. +pub(crate) struct GamepadSupport; + +impl GamepadSupport { + pub(crate) fn maybe_new() -> Option { + None + } + + pub(crate) fn handle_gamepad_events(&mut self, _active_webview: WebView) { + unreachable!("Dummy gamepad support should never be called."); + } + + pub(crate) fn play_haptic_effect( + &mut self, + _index: usize, + _effect_type: GamepadHapticEffectType, + _effect_complete_sender: IpcSender, + ) { + unreachable!("Dummy gamepad support should never be called."); + } + + pub(crate) fn stop_haptic_effect(&mut self, _index: usize) -> bool { + unreachable!("Dummy gamepad support should never be called."); + } +} diff --git a/ports/servoshell/egl/host_trait.rs b/ports/servoshell/egl/host_trait.rs index 192dc0ab34f..ab40db7d60f 100644 --- a/ports/servoshell/egl/host_trait.rs +++ b/ports/servoshell/egl/host_trait.rs @@ -2,22 +2,11 @@ * 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 servo::{ - InputMethodControl, LoadStatus, MediaSessionPlaybackState, PermissionRequest, SimpleDialog, - WebView, -}; +use servo::{InputMethodControl, LoadStatus, MediaSessionPlaybackState}; /// Callbacks implemented by embedder. Called by our RunningAppState, generally on behalf of Servo. pub trait HostTrait { - /// Content in a [`WebView`] is requesting permission to access a feature requiring - /// permission from the user. The embedder should allow or deny the request, either by - /// reading a cached value or querying the user for permission via the user interface. - fn request_permission(&self, _webview: WebView, _: PermissionRequest); - /// Show the user a [simple dialog](https://html.spec.whatwg.org/multipage/#simple-dialogs) (`alert()`, `confirm()`, - /// or `prompt()`). Since their messages are controlled by web content, they should be presented to the user in a - /// way that makes them impossible to mistake for browser UI. - /// TODO: This API needs to be reworked to match the new model of how responses are sent. - fn show_simple_dialog(&self, _webview: WebView, dialog: SimpleDialog); + fn show_alert(&self, message: String); /// Notify that the load status of the page has changed. /// Started: /// - "Reload button" should be disabled. @@ -30,8 +19,6 @@ pub trait HostTrait { fn notify_load_status_changed(&self, load_status: LoadStatus); /// Page title has changed. fn on_title_changed(&self, title: Option); - /// Allow Navigation. - fn on_allow_navigation(&self, url: String) -> bool; /// Page URL has changed. fn on_url_changed(&self, url: String); /// Back/forward state has changed. diff --git a/ports/servoshell/egl/mod.rs b/ports/servoshell/egl/mod.rs index 15010dc62c7..d97b5032b4f 100644 --- a/ports/servoshell/egl/mod.rs +++ b/ports/servoshell/egl/mod.rs @@ -4,11 +4,9 @@ #[cfg(target_os = "android")] mod android; - +mod app; +pub(crate) mod gamepad; +mod host_trait; +mod log; #[cfg(target_env = "ohos")] mod ohos; - -mod log; - -mod app_state; -mod host_trait; diff --git a/ports/servoshell/egl/ohos.rs b/ports/servoshell/egl/ohos/mod.rs similarity index 78% rename from ports/servoshell/egl/ohos.rs rename to ports/servoshell/egl/ohos/mod.rs index d02a5fb0bc8..467aa0ed168 100644 --- a/ports/servoshell/egl/ohos.rs +++ b/ports/servoshell/egl/ohos/mod.rs @@ -3,31 +3,44 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ #![allow(non_snake_case)] +mod resources; + use std::cell::RefCell; use std::mem::MaybeUninit; use std::os::raw::c_void; +use std::path::PathBuf; +use std::ptr::NonNull; use std::rc::Rc; use std::sync::mpsc::{Receiver, Sender}; use std::sync::{LazyLock, Mutex, Once, OnceLock, mpsc}; -use std::thread; use std::thread::sleep; use std::time::Duration; +use std::{fs, thread}; +use dpi::PhysicalSize; +use euclid::{Point2D, Rect, Size2D}; use keyboard_types::{Key, NamedKey}; use log::{LevelFilter, debug, error, info, trace, warn}; use napi_derive_ohos::napi; use napi_ohos::bindgen_prelude::{Function, JsObjectValue, Object}; use napi_ohos::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}; use napi_ohos::{Env, JsString, JsValue}; +use ohos_abilitykit_sys::runtime::application_context; use ohos_ime::{ AttachOptions, CreateImeProxyError, CreateTextEditorProxyError, Ime, ImeProxy, RawTextEditorProxy, }; use ohos_ime_sys::types::InputMethod_EnterKeyType; +use ohos_window_manager_sys::display_manager; +use raw_window_handle::{ + DisplayHandle, OhosDisplayHandle, OhosNdkWindowHandle, RawDisplayHandle, RawWindowHandle, + WindowHandle, +}; use servo::style::Zero; +use servo::webrender_api::units::DevicePixel; use servo::{ - AlertResponse, EventLoopWaker, InputMethodControl, InputMethodType, LoadStatus, - MediaSessionPlaybackState, PermissionRequest, SimpleDialog, WebView, WebViewId, + self, EventLoopWaker, InputMethodControl, InputMethodType, LoadStatus, + MediaSessionPlaybackState, WebViewId, WindowRenderingContext, }; use xcomponent_sys::{ OH_NativeXComponent, OH_NativeXComponent_Callback, OH_NativeXComponent_GetKeyEvent, @@ -39,12 +52,213 @@ use xcomponent_sys::{ OH_NativeXComponent_TouchEvent, OH_NativeXComponent_TouchEventType, }; -use super::app_state::{Coordinates, RunningAppState}; +use super::app::{App, AppInitOptions, VsyncRefreshDriver}; use super::host_trait::HostTrait; -use crate::running_app_state::RunningAppStateTrait; +use crate::egl::ohos::resources::ResourceReaderInstance; +use crate::prefs::{ArgumentParsingResult, parse_command_line_arguments}; -mod resources; -mod simpleservo; +/// Queue length for the thread-safe function to submit URL updates to ArkTS +const UPDATE_URL_QUEUE_SIZE: usize = 1; +/// Queue length for the thread-safe function to submit prompts to ArkTS +/// +/// We can submit alerts in a non-blocking fashion, but alerts will always come from the +/// embedder thread. Specifying 4 as a max queue size seems reasonable for now, and can +/// be adjusted later. +const PROMPT_QUEUE_SIZE: usize = 4; +// Todo: Need to check if OnceLock is suitable, or if the TS function can be destroyed, e.g. +// if the activity gets suspended. +static SET_URL_BAR_CB: OnceLock< + ThreadsafeFunction, +> = OnceLock::new(); +static TERMINATE_CALLBACK: OnceLock< + ThreadsafeFunction<(), (), (), napi_ohos::Status, false, false, 1>, +> = OnceLock::new(); +static PROMPT_TOAST: OnceLock< + ThreadsafeFunction, +> = OnceLock::new(); + +/// Currently we do not support different contexts for different windows but we might want to change tabs. +/// For this we store the window context for every tab and change the compositor by hand. +static NATIVE_WEBVIEWS: Mutex> = Mutex::new(Vec::new()); + +static SERVO_CHANNEL: OnceLock> = OnceLock::new(); + +pub(crate) fn get_raw_window_handle( + xcomponent: *mut OH_NativeXComponent, + window: *mut c_void, +) -> (RawWindowHandle, Rect) { + let window_size = unsafe { get_xcomponent_size(xcomponent, window) } + .expect("Could not get native window size"); + let window_origin = unsafe { get_xcomponent_offset(xcomponent, window) } + .expect("Could not get native window offset"); + let viewport_rect = Rect::new(window_origin, window_size); + let native_window = NonNull::new(window).expect("Could not get native window"); + let window_handle = RawWindowHandle::OhosNdk(OhosNdkWindowHandle::new(native_window)); + (window_handle, viewport_rect) +} + +#[derive(Debug)] +struct NativeValues { + cache_dir: String, + display_density: f32, + device_type: ohos_deviceinfo::OhosDeviceType, + os_full_name: String, +} + +/// Gets the resource and cache directory from the native c methods. +fn get_native_values() -> NativeValues { + let cache_dir = { + const BUFFER_SIZE: i32 = 100; + let mut buffer: Vec = Vec::with_capacity(BUFFER_SIZE as usize); + let mut write_length = 0; + unsafe { + application_context::OH_AbilityRuntime_ApplicationContextGetCacheDir( + buffer.as_mut_ptr().cast(), + BUFFER_SIZE, + &mut write_length, + ) + .expect("Call to cache dir failed"); + buffer.set_len(write_length as usize); + String::from_utf8(buffer).expect("UTF-8") + } + }; + let display_density = unsafe { + let mut density: f32 = 0_f32; + display_manager::OH_NativeDisplayManager_GetDefaultDisplayDensityPixels(&mut density) + .expect("Could not get displaydensity"); + density + }; + + NativeValues { + cache_dir, + display_density, + device_type: ohos_deviceinfo::get_device_type(), + os_full_name: String::from(ohos_deviceinfo::get_os_full_name().unwrap_or("Undefined")), + } +} + +/// Initialize the servoshell [`App`]. At that point, we need a valid GL context. In the +/// future, this will be done in multiple steps. +fn init_app( + options: InitOpts, + native_window: *mut c_void, + xcomponent: *mut OH_NativeXComponent, + event_loop_waker: Box, + host: Box, +) -> Result, &'static str> { + info!("Entered servoshell init function"); + crate::init_crypto(); + + let native_values = get_native_values(); + info!("Device Type {:?}", native_values.device_type); + info!("OS Full Name {:?}", native_values.os_full_name); + info!("ResourceDir {:?}", options.resource_dir); + + let resource_dir = PathBuf::from(&options.resource_dir).join("servo"); + debug!("Resources are located at: {:?}", resource_dir); + servo::resources::set(Box::new(ResourceReaderInstance::new(resource_dir.clone()))); + + // It would be nice if `from_cmdline_args()` could accept str slices, to avoid allocations here. + // Then again, this code could and maybe even should be disabled in production builds. + let mut args = vec!["servoshell".to_string()]; + args.extend( + options + .commandline_args + .split("\u{1f}") + .map(|arg| arg.to_string()), + ); + debug!("Servo commandline args: {:?}", args); + + let config_dir = PathBuf::from(&native_values.cache_dir).join("servo"); + debug!("Configs are located at: {:?}", config_dir); + let _ = crate::prefs::DEFAULT_CONFIG_DIR + .set(config_dir.clone()) + .inspect_err(|e| { + warn!( + "Default Prefs Dir already previously filled. Got error {}", + e.display() + ); + }); + + // Ensure cache dir exists before copy `prefs.json` + let _ = crate::prefs::default_config_dir().inspect(|path| { + if !path.exists() { + fs::create_dir_all(path).unwrap_or_else(|e| { + log::error!("Failed to create config directory at {:?}: {:?}", path, e) + }) + } + }); + + // Try copy `prefs.json` from {this.context.resource_prefsDir}/servo/ + // to `config_dir` if none exist + let source_prefs = resource_dir.join("prefs.json"); + let target_prefs = config_dir.join("prefs.json"); + if !target_prefs.exists() && source_prefs.exists() { + debug!("Copy {:?} to {:?}", source_prefs, target_prefs); + fs::copy(&source_prefs, &target_prefs).unwrap_or_else(|e| { + debug!("Copy failed! {:?}", e); + 0 + }); + } + + let (opts, mut preferences, servoshell_preferences) = match parse_command_line_arguments(args) { + ArgumentParsingResult::ContentProcess(..) => { + unreachable!("OHOS does not have support for multiprocess yet.") + }, + ArgumentParsingResult::ChromeProcess(opts, preferences, servoshell_preferences) => { + (opts, preferences, servoshell_preferences) + }, + ArgumentParsingResult::Exit => std::process::exit(0), + ArgumentParsingResult::ErrorParsing => std::process::exit(1), + }; + + if native_values.device_type == ohos_deviceinfo::OhosDeviceType::Phone { + preferences.set_value("viewport_meta_enabled", servo::PrefValue::Bool(true)); + } + + if servoshell_preferences.log_to_file { + let mut servo_log = PathBuf::from(&native_values.cache_dir); + servo_log.push("servo.log"); + if crate::egl::ohos::LOGGER.set_file_writer(servo_log).is_err() { + warn!("Could not set log file"); + } + } + + crate::init_tracing(servoshell_preferences.tracing_filter.as_deref()); + #[cfg(target_env = "ohos")] + crate::egl::ohos::set_log_filter(servoshell_preferences.log_filter.as_deref()); + + let (window_handle, viewport_rect) = get_raw_window_handle(xcomponent, native_window); + let display_handle = RawDisplayHandle::Ohos(OhosDisplayHandle::new()); + let display_handle = unsafe { DisplayHandle::borrow_raw(display_handle) }; + let window_handle = unsafe { WindowHandle::borrow_raw(window_handle) }; + + let viewport_size = viewport_rect.size; + let refresh_driver = Rc::new(VsyncRefreshDriver::default()); + let rendering_context = Rc::new( + WindowRenderingContext::new_with_refresh_driver( + display_handle, + window_handle, + PhysicalSize::new(viewport_size.width as u32, viewport_size.height as u32), + refresh_driver.clone(), + ) + .expect("Could not create RenderingContext"), + ); + Ok(App::new(AppInitOptions { + host, + event_loop_waker, + viewport_rect, + hidpi_scale_factor: native_values.display_density as f32, + rendering_context, + refresh_driver, + initial_url: Some(options.url), + opts, + preferences, + servoshell_preferences, + #[cfg(feature = "webxr")] + xr_discovery: None, + })) +} #[napi(object)] #[derive(Debug)] @@ -114,26 +328,6 @@ pub(super) enum ServoAction { NewWebview(XComponentWrapper, WindowWrapper), } -/// Queue length for the thread-safe function to submit URL updates to ArkTS -const UPDATE_URL_QUEUE_SIZE: usize = 1; -/// Queue length for the thread-safe function to submit prompts to ArkTS -/// -/// We can submit alerts in a non-blocking fashion, but alerts will always come from the -/// embedder thread. Specifying 4 as a max queue size seems reasonable for now, and can -/// be adjusted later. -const PROMPT_QUEUE_SIZE: usize = 4; -// Todo: Need to check if OnceLock is suitable, or if the TS function can be destroyed, e.g. -// if the activity gets suspended. -static SET_URL_BAR_CB: OnceLock< - ThreadsafeFunction, -> = OnceLock::new(); -static TERMINATE_CALLBACK: OnceLock< - ThreadsafeFunction<(), (), (), napi_ohos::Status, false, false, 1>, -> = OnceLock::new(); -static PROMPT_TOAST: OnceLock< - ThreadsafeFunction, -> = OnceLock::new(); - /// Storing webview related items struct NativeWebViewComponents { /// The id of the related webview @@ -144,18 +338,8 @@ struct NativeWebViewComponents { window: WindowWrapper, } -/// Currently we do not support different contexts for different windows but we might want to change tabs. -/// For this we store the window context for every tab and change the compositor by hand. -static NATIVE_WEBVIEWS: Mutex> = Mutex::new(Vec::new()); - impl ServoAction { - fn dispatch_touch_event( - servo: &RunningAppState, - kind: TouchEventType, - x: f32, - y: f32, - pointer_id: i32, - ) { + fn dispatch_touch_event(servo: &App, kind: TouchEventType, x: f32, y: f32, pointer_id: i32) { match kind { TouchEventType::Down => servo.touch_down(x, y, pointer_id), TouchEventType::Up => servo.touch_up(x, y, pointer_id), @@ -166,12 +350,11 @@ impl ServoAction { } // todo: consider making this take `self`, so we don't need to needlessly clone. - fn do_action(&self, servo: &Rc) { + fn do_action(&self, servo: &Rc) { use ServoAction::*; match self { WakeUp => { - servo.perform_updates(); - servo.present_if_needed(); + servo.spin_event_loop(); }, LoadUrl(url) => servo.load_uri(url.as_str()), GoBack => servo.go_back(), @@ -206,23 +389,26 @@ impl ServoAction { }, Vsync => { servo.notify_vsync(); - servo.present_if_needed(); }, - Resize { width, height } => servo.resize(Coordinates::new(0, 0, *width, *height)), + Resize { width, height } => { + servo.resize(Rect::new(Point2D::origin(), Size2D::new(*width, *height))) + }, FocusWebview(arkts_id) => { if let Some(native_webview_components) = NATIVE_WEBVIEWS.lock().unwrap().get(*arkts_id as usize) { - if servo.active_webview_or_panic().id() != native_webview_components.id { + let webview = servo + .focused_or_newest_webview() + .expect("Should always start with at least one WebView"); + if webview.id() != native_webview_components.id { servo.focus_webview(native_webview_components.id); servo.pause_compositor(); - let (window_handle, _, coordinates) = simpleservo::get_raw_window_handle( + let (window_handle, viewport_rect) = get_raw_window_handle( native_webview_components.xcomponent.0, native_webview_components.window.0, ); - servo.resume_compositor(window_handle, coordinates); - let url = servo - .active_webview_or_panic() + servo.resume_compositor(window_handle, viewport_rect); + let url = webview .url() .map(|u| u.to_string()) .unwrap_or(String::from("about:blank")); @@ -236,12 +422,11 @@ impl ServoAction { }, NewWebview(xcomponent, window) => { servo.pause_compositor(); - servo.create_and_focus_toplevel_webview("about:blank".parse().unwrap()); - let (window_handle, _, coordinates) = - simpleservo::get_raw_window_handle(xcomponent.0, window.0); + let webview = + servo.create_and_focus_toplevel_webview("about:blank".parse().unwrap()); + let (window_handle, viewport_rect) = get_raw_window_handle(xcomponent.0, window.0); - servo.resume_compositor(window_handle, coordinates); - let webview = servo.newest_webview().expect("There should always be one"); + servo.resume_compositor(window_handle, viewport_rect); let id = webview.id(); NATIVE_WEBVIEWS .lock() @@ -286,8 +471,6 @@ unsafe extern "C" fn on_vsync_cb( } } -static SERVO_CHANNEL: OnceLock> = OnceLock::new(); - #[unsafe(no_mangle)] extern "C" fn on_surface_created_cb(xcomponent: *mut OH_NativeXComponent, window: *mut c_void) { info!("on_surface_created_cb"); @@ -320,14 +503,17 @@ extern "C" fn on_surface_created_cb(xcomponent: *mut OH_NativeXComponent, window } else { panic!("Servos GL thread received another event before it was initialized") }; - let servo = simpleservo::init(*init_opts, window.0, xc.0, wakeup, callbacks) + let servo = init_app(*init_opts, window.0, xc.0, wakeup, callbacks) .expect("Servo initialization failed"); - + let id = servo + .focused_or_newest_webview() + .expect("Should always start with at least one WebView") + .id(); NATIVE_WEBVIEWS .lock() .unwrap() .push(NativeWebViewComponents { - id: servo.active_webview_or_panic().id(), + id, xcomponent: xc, window, }); @@ -347,8 +533,6 @@ extern "C" fn on_surface_created_cb(xcomponent: *mut OH_NativeXComponent, window while let Ok(action) = rx.recv() { trace!("Wakeup message received!"); action.do_action(&servo); - // Also handle any pending WebDriver messages - servo.handle_webdriver_messages(); } info!("Sender disconnected - Terminating main surface thread"); @@ -369,7 +553,7 @@ extern "C" fn on_surface_created_cb(xcomponent: *mut OH_NativeXComponent, window unsafe fn get_xcomponent_offset( xcomponent: *mut OH_NativeXComponent, native_window: *mut c_void, -) -> Result<(i32, i32), i32> { +) -> Result, i32> { let mut x: f64 = 0.0; let mut y: f64 = 0.0; @@ -380,11 +564,9 @@ unsafe fn get_xcomponent_offset( error!("OH_NativeXComponent_GetXComponentOffset failed with {result}"); return Err(result); } - - Ok(( - (x.round() as i64).try_into().expect("X offset too large"), - (y.round() as i64).try_into().expect("Y offset too large"), - )) + let x = (x.round() as i64).try_into().expect("X offset too large"); + let y = (y.round() as i64).try_into().expect("Y offset too large"); + Ok(Point2D::new(x, y)) } /// Returns the size of the surface @@ -396,7 +578,7 @@ unsafe fn get_xcomponent_offset( unsafe fn get_xcomponent_size( xcomponent: *mut OH_NativeXComponent, native_window: *mut c_void, -) -> Result, i32> { +) -> Result, i32> { let mut width: u64 = 0; let mut height: u64 = 0; let result = unsafe { @@ -414,7 +596,7 @@ unsafe fn get_xcomponent_size( let width: i32 = width.try_into().expect("Width too large"); let height: i32 = height.try_into().expect("Height too large"); - Ok(euclid::Size2D::new(width, height)) + Ok(Size2D::new(width, height)) } extern "C" fn on_surface_changed_cb( @@ -652,7 +834,7 @@ fn debug_jsobject(obj: &Object, obj_name: &str) -> napi_ohos::Result<()> { #[napi(module_exports)] fn init(exports: Object, env: Env) -> napi_ohos::Result<()> { initialize_logging_once(); - info!("simpleservo init function called"); + info!("servoshell init function called"); if let Ok(xcomponent) = exports.get_named_property::("__NATIVE_XCOMPONENT_OBJ__") { register_xcomponent_callbacks(&env, &xcomponent)?; } @@ -836,19 +1018,6 @@ impl HostCallbacks { } } - pub fn show_alert(&self, message: String) { - match PROMPT_TOAST.get() { - Some(prompt_fn) => { - let status = prompt_fn.call(message, ThreadsafeFunctionCallMode::NonBlocking); - if status != napi_ohos::Status::Ok { - // Queue could be full. - error!("show_alert failed with {status}"); - } - }, - None => error!("PROMPT_TOAST not set. Dropping message {message}"), - } - } - fn try_create_ime_proxy( &self, input_type: InputMethodType, @@ -891,51 +1060,27 @@ impl Ime for ServoIme { #[allow(unused)] impl HostTrait for HostCallbacks { - fn request_permission(&self, _webview: WebView, request: PermissionRequest) { - warn!("Permissions prompt not implemented. Denied."); - request.deny(); - } - - fn show_simple_dialog(&self, _webview: WebView, dialog: SimpleDialog) { - let _ = match dialog { - SimpleDialog::Alert { - message, - response_sender, - .. - } => { - debug!("SimpleDialog::Alert"); - - // forward it to tracing - #[cfg(feature = "tracing-hitrace")] - { - if message.contains("TESTCASE_PROFILING") { - if let Some((tag, number)) = message.rsplit_once(":") { - hitrace::trace_metric_str(tag, number.parse::().unwrap_or(-1)); - } - } + fn show_alert(&self, message: String) { + // forward it to tracing + #[cfg(feature = "tracing-hitrace")] + { + if message.contains("TESTCASE_PROFILING") { + if let Some((tag, number)) = message.rsplit_once(":") { + hitrace::trace_metric_str(tag, number.parse::().unwrap_or(-1)); } + } + } - // TODO: Indicate that this message is untrusted, and what origin it came from. - self.show_alert(message); - response_sender.send(AlertResponse::Ok) + match PROMPT_TOAST.get() { + Some(prompt_fn) => { + let status = prompt_fn.call(message, ThreadsafeFunctionCallMode::NonBlocking); + if status != napi_ohos::Status::Ok { + // Queue could be full. + error!("show_alert failed with {status}"); + } }, - SimpleDialog::Confirm { - message, - response_sender, - .. - } => { - warn!("Confirm dialog not implemented. Cancelled. {}", message); - response_sender.send(Default::default()) - }, - SimpleDialog::Prompt { - message, - response_sender, - .. - } => { - warn!("Prompt dialog not implemented. Cancelled. {}", message); - response_sender.send(Default::default()) - }, - }; + None => error!("PROMPT_TOAST not set. Dropping message {message}"), + } } fn notify_load_status_changed(&self, load_status: LoadStatus) { @@ -961,10 +1106,6 @@ impl HostTrait for HostCallbacks { warn!("on_title_changed not implemented") } - fn on_allow_navigation(&self, url: String) -> bool { - true - } - fn on_url_changed(&self, url: String) { debug!("Hosttrait `on_url_changed` called with new url: {url}"); match SET_URL_BAR_CB.get() { diff --git a/ports/servoshell/egl/ohos/simpleservo.rs b/ports/servoshell/egl/ohos/simpleservo.rs deleted file mode 100644 index ffa446e59da..00000000000 --- a/ports/servoshell/egl/ohos/simpleservo.rs +++ /dev/null @@ -1,228 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use std::cell::RefCell; -use std::fs; -use std::os::raw::c_void; -use std::path::PathBuf; -use std::ptr::NonNull; -use std::rc::Rc; - -use crossbeam_channel::unbounded; -use dpi::PhysicalSize; -use log::{debug, info, warn}; -use ohos_abilitykit_sys::runtime::application_context; -use ohos_window_manager_sys::display_manager; -use raw_window_handle::{ - DisplayHandle, OhosDisplayHandle, OhosNdkWindowHandle, RawDisplayHandle, RawWindowHandle, - WindowHandle, -}; -use servo::{self, EventLoopWaker, ServoBuilder, WindowRenderingContext, resources}; -use xcomponent_sys::OH_NativeXComponent; - -use crate::egl::app_state::{ - Coordinates, RunningAppState, ServoWindowCallbacks, VsyncRefreshDriver, -}; -use crate::egl::host_trait::HostTrait; -use crate::egl::ohos::InitOpts; -use crate::egl::ohos::resources::ResourceReaderInstance; -use crate::prefs::{ArgumentParsingResult, parse_command_line_arguments}; - -pub(crate) fn get_raw_window_handle( - xcomponent: *mut OH_NativeXComponent, - window: *mut c_void, -) -> (RawWindowHandle, euclid::default::Size2D, Coordinates) { - let window_size = unsafe { super::get_xcomponent_size(xcomponent, window) } - .expect("Could not get native window size"); - let (x, y) = unsafe { super::get_xcomponent_offset(xcomponent, window) } - .expect("Could not get native window offset"); - let coordinates = Coordinates::new(x, y, window_size.width, window_size.height); - let native_window = NonNull::new(window).expect("Could not get native window"); - let window_handle = RawWindowHandle::OhosNdk(OhosNdkWindowHandle::new(native_window)); - (window_handle, window_size, coordinates) -} - -#[derive(Debug)] -struct NativeValues { - cache_dir: String, - display_density: f32, - device_type: ohos_deviceinfo::OhosDeviceType, - os_full_name: String, -} - -/// Gets the resource and cache directory from the native c methods. -fn get_native_values() -> NativeValues { - let cache_dir = { - const BUFFER_SIZE: i32 = 100; - let mut buffer: Vec = Vec::with_capacity(BUFFER_SIZE as usize); - let mut write_length = 0; - unsafe { - application_context::OH_AbilityRuntime_ApplicationContextGetCacheDir( - buffer.as_mut_ptr().cast(), - BUFFER_SIZE, - &mut write_length, - ) - .expect("Call to cache dir failed"); - buffer.set_len(write_length as usize); - String::from_utf8(buffer).expect("UTF-8") - } - }; - let display_density = unsafe { - let mut density: f32 = 0_f32; - display_manager::OH_NativeDisplayManager_GetDefaultDisplayDensityPixels(&mut density) - .expect("Could not get displaydensity"); - density - }; - - NativeValues { - cache_dir, - display_density, - device_type: ohos_deviceinfo::get_device_type(), - os_full_name: String::from(ohos_deviceinfo::get_os_full_name().unwrap_or("Undefined")), - } -} - -/// Initialize Servo. At that point, we need a valid GL context. -/// In the future, this will be done in multiple steps. -pub fn init( - options: InitOpts, - native_window: *mut c_void, - xcomponent: *mut OH_NativeXComponent, - waker: Box, - callbacks: Box, -) -> Result, &'static str> { - info!("Entered simpleservo init function"); - crate::init_crypto(); - - let native_values = get_native_values(); - info!("Device Type {:?}", native_values.device_type); - info!("OS Full Name {:?}", native_values.os_full_name); - info!("ResourceDir {:?}", options.resource_dir); - - let resource_dir = PathBuf::from(&options.resource_dir).join("servo"); - debug!("Resources are located at: {:?}", resource_dir); - resources::set(Box::new(ResourceReaderInstance::new(resource_dir.clone()))); - - // It would be nice if `from_cmdline_args()` could accept str slices, to avoid allocations here. - // Then again, this code could and maybe even should be disabled in production builds. - let mut args = vec!["servoshell".to_string()]; - args.extend( - options - .commandline_args - .split("\u{1f}") - .map(|arg| arg.to_string()), - ); - debug!("Servo commandline args: {:?}", args); - - let config_dir = PathBuf::from(&native_values.cache_dir).join("servo"); - debug!("Configs are located at: {:?}", config_dir); - let _ = crate::prefs::DEFAULT_CONFIG_DIR - .set(config_dir.clone()) - .inspect_err(|e| { - warn!( - "Default Prefs Dir already previously filled. Got error {}", - e.display() - ); - }); - - // Ensure cache dir exists before copy `prefs.json` - let _ = crate::prefs::default_config_dir().inspect(|path| { - if !path.exists() { - fs::create_dir_all(path).unwrap_or_else(|e| { - log::error!("Failed to create config directory at {:?}: {:?}", path, e) - }) - } - }); - - // Try copy `prefs.json` from {this.context.resource_prefsDir}/servo/ - // to `config_dir` if none exist - let source_prefs = resource_dir.join("prefs.json"); - let target_prefs = config_dir.join("prefs.json"); - if !target_prefs.exists() && source_prefs.exists() { - debug!("Copy {:?} to {:?}", source_prefs, target_prefs); - fs::copy(&source_prefs, &target_prefs).unwrap_or_else(|e| { - debug!("Copy failed! {:?}", e); - 0 - }); - } - - let (opts, mut preferences, servoshell_preferences) = match parse_command_line_arguments(args) { - ArgumentParsingResult::ContentProcess(..) => { - unreachable!("OHOS does not have support for multiprocess yet.") - }, - ArgumentParsingResult::ChromeProcess(opts, preferences, servoshell_preferences) => { - (opts, preferences, servoshell_preferences) - }, - ArgumentParsingResult::Exit => std::process::exit(0), - ArgumentParsingResult::ErrorParsing => std::process::exit(1), - }; - - if native_values.device_type == ohos_deviceinfo::OhosDeviceType::Phone { - preferences.set_value("viewport_meta_enabled", servo::PrefValue::Bool(true)); - } - - if servoshell_preferences.log_to_file { - let mut servo_log = PathBuf::from(&native_values.cache_dir); - servo_log.push("servo.log"); - if crate::egl::ohos::LOGGER.set_file_writer(servo_log).is_err() { - warn!("Could not set log file"); - } - } - - crate::init_tracing(servoshell_preferences.tracing_filter.as_deref()); - #[cfg(target_env = "ohos")] - crate::egl::ohos::set_log_filter(servoshell_preferences.log_filter.as_deref()); - - let (window_handle, window_size, coordinates) = - get_raw_window_handle(xcomponent, native_window); - - let display_handle = RawDisplayHandle::Ohos(OhosDisplayHandle::new()); - let display_handle = unsafe { DisplayHandle::borrow_raw(display_handle) }; - - let window_handle = unsafe { WindowHandle::borrow_raw(window_handle) }; - - let refresh_driver = Rc::new(VsyncRefreshDriver::default()); - let rendering_context = Rc::new( - WindowRenderingContext::new_with_refresh_driver( - display_handle, - window_handle, - PhysicalSize::new(window_size.width as u32, window_size.height as u32), - refresh_driver.clone(), - ) - .expect("Could not create RenderingContext"), - ); - - info!("before ServoWindowCallbacks..."); - - let window_callbacks = Rc::new(ServoWindowCallbacks::new( - callbacks, - RefCell::new(coordinates), - )); - - let servo = ServoBuilder::default() - .opts(opts) - .preferences(preferences) - .event_loop_waker(waker.clone()) - .build(); - - // Initialize WebDriver server if port is specified - let webdriver_receiver = servoshell_preferences.webdriver_port.map(|port| { - let (embedder_sender, embedder_receiver) = unbounded(); - webdriver_server::start_server(port, embedder_sender, waker); - log::info!("WebDriver server started on port {port}"); - embedder_receiver - }); - - let app_state = RunningAppState::new( - Some(options.url), - native_values.display_density as f32, - rendering_context, - servo, - window_callbacks, - Some(refresh_driver), - servoshell_preferences, - webdriver_receiver, - ); - - Ok(app_state) -} diff --git a/ports/servoshell/lib.rs b/ports/servoshell/lib.rs index 8f3531fd4a9..e0879cfeb69 100644 --- a/ports/servoshell/lib.rs +++ b/ports/servoshell/lib.rs @@ -19,9 +19,16 @@ mod egl; mod panic_hook; mod parser; mod prefs; -#[cfg(not(any(target_os = "android", target_env = "ohos")))] +#[cfg(not(target_os = "android"))] mod resources; mod running_app_state; +mod webdriver; +mod window; + +#[cfg(not(any(target_os = "android", target_env = "ohos")))] +pub(crate) use crate::desktop::gamepad::GamepadSupport; +#[cfg(any(target_os = "android", target_env = "ohos"))] +pub(crate) use crate::egl::gamepad::GamepadSupport; pub mod platform { #[cfg(target_os = "macos")] diff --git a/ports/servoshell/main.rs b/ports/servoshell/main.rs index 77406dba5d4..063abbc8724 100644 --- a/ports/servoshell/main.rs +++ b/ports/servoshell/main.rs @@ -25,8 +25,8 @@ fn main() { if #[cfg(not(any(target_os = "android", target_env = "ohos")))] { servoshell::main() } else { - // Android: see ports/servoshell/egl/android/simpleservo.rs. - // OpenHarmony: see ports/servoshell/egl/ohos/simpleservo.rs. + // Android: see ports/servoshell/egl/android/mod.rs. + // OpenHarmony: see ports/servoshell/egl/ohos/mod.rs. println!( "Cannot run the servoshell `bin` executable on platforms such as \ Android or OpenHarmony. On these platforms you need to compile \ diff --git a/ports/servoshell/running_app_state.rs b/ports/servoshell/running_app_state.rs index 1df348d98a3..e02c539ab96 100644 --- a/ports/servoshell/running_app_state.rs +++ b/ports/servoshell/running_app_state.rs @@ -4,25 +4,34 @@ //! Shared state and methods for desktop and EGL implementations. -use std::cell::{Cell, RefCell}; +use std::cell::{Cell, Ref, RefCell}; use std::collections::HashMap; +use std::collections::hash_map::Entry; use std::rc::Rc; -use crossbeam_channel::{Receiver, Sender}; +use crossbeam_channel::{Receiver, Sender, unbounded}; use euclid::Rect; use image::{DynamicImage, ImageFormat, RgbaImage}; use log::{error, info, warn}; use servo::base::generic_channel::GenericSender; use servo::base::id::WebViewId; +use servo::config::pref; use servo::ipc_channel::ipc::IpcSender; use servo::style_traits::CSSPixel; +use servo::webrender_api::units::{DeviceIntPoint, DeviceIntSize}; use servo::{ - InputEvent, InputEventId, ScreenshotCaptureError, Servo, TraversalId, WebDriverCommandMsg, - WebDriverJSResult, WebDriverLoadStatus, WebDriverScriptCommand, WebDriverSenders, WebView, + AllowOrDenyRequest, AuthenticationRequest, EmbedderControl, EmbedderControlId, EventLoopWaker, + GamepadHapticEffectType, InputEvent, InputEventId, InputEventResult, JSValue, LoadStatus, + MediaSessionEvent, PermissionRequest, ScreenshotCaptureError, Servo, ServoDelegate, ServoError, + TraversalId, WebDriverCommandMsg, WebDriverJSResult, WebDriverLoadStatus, + WebDriverScriptCommand, WebDriverSenders, WebView, WebViewBuilder, WebViewDelegate, }; use url::Url; +use crate::GamepadSupport; use crate::prefs::ServoShellPreferences; +use crate::webdriver::WebDriverEmbedderControls; +use crate::window::{PlatformWindow, ServoShellWindow, ServoShellWindowId}; #[derive(Default)] pub struct WebViewCollection { @@ -32,7 +41,7 @@ pub struct WebViewCollection { webviews: HashMap, /// The order in which the webviews were created. - creation_order: Vec, + pub(crate) creation_order: Vec, /// The webview that is currently focused. /// Modified by EmbedderMsg::WebViewFocused and EmbedderMsg::WebViewBlurred. @@ -85,46 +94,35 @@ impl WebViewCollection { .and_then(|id| self.webviews.get(id)) } - /// Gets the "active" webview: the focused webview if there is one, - /// otherwise the most recently created webview. - #[allow(dead_code)] - pub fn active(&self) -> Option<&WebView> { - self.focused().or_else(|| self.newest()) - } - pub fn all_in_creation_order(&self) -> impl Iterator { self.creation_order .iter() .filter_map(move |id| self.webviews.get(id).map(|webview| (*id, webview))) } - pub fn clear(&mut self) { - self.webviews.clear(); - self.creation_order.clear(); - self.focused_webview_id = None; - } - /// Returns an iterator over all webview references (in arbitrary order). pub fn values(&self) -> impl Iterator { self.webviews.values() } - pub fn len(&self) -> usize { - self.webviews.len() - } - /// Returns true if the collection contains no webviews. - #[allow(dead_code)] pub fn is_empty(&self) -> bool { self.webviews.is_empty() } } -pub struct RunningAppStateBase { - pub(crate) webview_collection: RefCell, +pub(crate) struct RunningAppState { + /// Gamepad support, which may be `None` if it failed to initialize. + gamepad_support: RefCell>, + /// The [`WebDriverSenders`] used to reply to pending WebDriver requests. pub(crate) webdriver_senders: RefCell, + /// When running in WebDriver mode, [`WebDriverEmbedderControls`] is a virtual container + /// for all embedder controls. This overrides the normal behavior where these controls + /// are shown in the GUI or not processed at all in headless mode. + pub(crate) webdriver_embedder_controls: WebDriverEmbedderControls, + /// A [`HashMap`] of pending WebDriver events. It is the WebDriver embedder's responsibility /// to inform the WebDriver server when the event has been fully handled. This map is used /// to report back to WebDriver when that happens. @@ -143,17 +141,45 @@ pub struct RunningAppStateBase { /// Whether or not the application has achieved stable image output. This is used /// for the `exit_after_stable_image` option. pub(crate) achieved_stable_image: Rc>, + + /// The set of [`ServoShellWindow`]s that currently exist for this instance of servoshell. + // This is the last field of the struct to ensure that windows are dropped *after* all + // other references to the relevant rendering contexts have been destroyed. + // See https://github.com/servo/servo/issues/36711. + windows: RefCell>>, } -impl RunningAppStateBase { - pub fn new( - servoshell_preferences: ServoShellPreferences, +impl Drop for RunningAppState { + fn drop(&mut self) { + self.servo.deinit(); + } +} + +impl RunningAppState { + pub(crate) fn new( servo: Servo, - webdriver_receiver: Option>, + servoshell_preferences: ServoShellPreferences, + event_loop_waker: Box, ) -> Self { + servo.set_delegate(Rc::new(ServoShellServoDelegate)); + + let gamepad_support = if pref!(dom_gamepad_enabled) { + GamepadSupport::maybe_new() + } else { + None + }; + + let webdriver_receiver = servoshell_preferences.webdriver_port.map(|port| { + let (embedder_sender, embedder_receiver) = unbounded(); + webdriver_server::start_server(port, embedder_sender, event_loop_waker); + embedder_receiver + }); + Self { - webview_collection: RefCell::default(), + windows: Default::default(), + gamepad_support: RefCell::new(gamepad_support), webdriver_senders: RefCell::default(), + webdriver_embedder_controls: Default::default(), pending_webdriver_events: Default::default(), webdriver_receiver, servoshell_preferences, @@ -161,200 +187,134 @@ impl RunningAppStateBase { achieved_stable_image: Default::default(), } } -} -pub trait RunningAppStateTrait { - fn base(&self) -> &RunningAppStateBase; - #[expect(dead_code)] - fn base_mut(&mut self) -> &mut RunningAppStateBase; - fn webview_by_id(&self, _: WebViewId) -> Option; - fn dismiss_embedder_controls_for_webview(&self, _webview_id: WebViewId) {} - - fn servoshell_preferences(&self) -> &ServoShellPreferences { - &self.base().servoshell_preferences - } - - fn servo(&self) -> &Servo { - &self.base().servo - } - - fn webdriver_receiver(&self) -> Option<&Receiver> { - self.base().webdriver_receiver.as_ref() - } - - fn webview_collection(&self) -> std::cell::Ref<'_, WebViewCollection> { - self.base().webview_collection.borrow() - } - - fn webview_collection_mut(&self) -> std::cell::RefMut<'_, WebViewCollection> { - self.base().webview_collection.borrow_mut() - } - - /// Returns all [`WebView`]s in creation order. - fn webviews(&self) -> Vec<(WebViewId, WebView)> { - self.webview_collection() - .all_in_creation_order() - .map(|(id, webview)| (id, webview.clone())) - .collect() - } - - fn add_webview(&self, webview: WebView) { - let webview_id = webview.id(); - self.webview_collection_mut().add(webview); - let total = self.webview_collection().len(); - info!("Added webview with ID: {webview_id:?}, total webviews: {total}"); - } - - fn focused_webview(&self) -> Option { - self.webview_collection().focused().cloned() - } - - #[allow(dead_code)] - fn newest_webview(&self) -> Option { - self.webview_collection().newest().cloned() - } - - /// Gets the "active" [`WebView`]: the focused [`WebView`] if there is one, - /// otherwise the most recently created [`WebView`]. - #[allow(dead_code)] - fn active_webview(&self) -> Option { - self.webview_collection().active().cloned() - } - - /// Gets the "active" [`WebView`], panicking if there is none. - /// This is a convenience method for platforms that assume there's always an active [`WebView`]. - #[allow(dead_code)] - fn active_webview_or_panic(&self) -> WebView { - self.active_webview() - .expect("Should always have an active WebView") - } - - fn set_pending_traversal( - &self, - traversal_id: TraversalId, - sender: GenericSender, + pub(crate) fn create_window( + self: &Rc, + platform_window: Rc, + initial_url: Url, ) { - self.base() - .webdriver_senders - .borrow_mut() - .pending_traversals - .insert(traversal_id, sender); + let window = Rc::new(ServoShellWindow::new(platform_window)); + window.create_and_focus_toplevel_webview(self.clone(), initial_url); + self.windows.borrow_mut().insert(window.id(), window); } - fn set_load_status_sender( - &self, - webview_id: WebViewId, - sender: GenericSender, - ) { - self.base() - .webdriver_senders - .borrow_mut() - .load_status_senders - .insert(webview_id, sender); + pub(crate) fn windows<'a>( + &'a self, + ) -> Ref<'a, HashMap>> { + self.windows.borrow() } - fn remove_load_status_sender(&self, webview_id: WebViewId) { - self.base() - .webdriver_senders - .borrow_mut() - .load_status_senders - .remove(&webview_id); + /// Get any [`ServoShellWindow`] from this state's collection of windows. This is used for + /// WebDriver, which currently doesn't have great support for per-window operation. + pub(crate) fn any_window(&self) -> Rc { + self.windows + .borrow() + .values() + .next() + .expect("Should always have at least one window open when running WebDriver") + .clone() } - fn set_script_command_interrupt_sender(&self, sender: Option>) { - self.base() - .webdriver_senders - .borrow_mut() - .script_evaluation_interrupt_sender = sender; + pub(crate) fn focused_window(&self) -> Option> { + self.windows + .borrow() + .values() + .find(|window| window.focused()) + .cloned() } - fn handle_webdriver_input_event( - &self, - webview_id: WebViewId, - input_event: InputEvent, - response_sender: Option>, - ) { - if let Some(webview) = self.webview_by_id(webview_id) { - let event_id = webview.notify_input_event(input_event); - if let Some(response_sender) = response_sender { - self.base() - .pending_webdriver_events - .borrow_mut() - .insert(event_id, response_sender); + #[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))] + pub(crate) fn window(&self, id: ServoShellWindowId) -> Option> { + self.windows.borrow().get(&id).cloned() + } + + pub(crate) fn webview_by_id(&self, webview_id: WebViewId) -> Option { + self.maybe_window_for_webview_id(webview_id)? + .webview_by_id(webview_id) + } + + pub(crate) fn webdriver_receiver(&self) -> Option<&Receiver> { + self.webdriver_receiver.as_ref() + } + + pub(crate) fn servo(&self) -> &Servo { + &self.servo + } + + /// Spins the internal application event loop. + /// + /// - Notifies Servo about incoming gamepad events + /// - Spin the Servo event loop, which will run the compositor and trigger delegate methods. + /// + /// Returns true if the event loop should continue spinning and false if it should exit. + pub(crate) fn spin_event_loop(self: &Rc) -> bool { + self.handle_webdriver_messages(); + + if pref!(dom_gamepad_enabled) { + self.handle_gamepad_events(); + } + + if !self.servo.spin_event_loop() { + return false; + } + + for window in self.windows.borrow().values() { + window.update_and_request_repaint_if_necessary(self); + } + + if self.servoshell_preferences.exit_after_stable_image && self.achieved_stable_image.get() { + self.servo.start_shutting_down(); + } + + // When a ServoShellWindow has no more WebViews, close it. When no more windows are open, exit + // the application. Do not do this when running WebDriver, which expects to keep running with + // no WebView open. + if self.servoshell_preferences.webdriver_port.is_none() { + self.windows + .borrow_mut() + .retain(|_, window| !window.should_close()); + if self.windows.borrow().is_empty() { + self.servo.start_shutting_down(); } - } else { - error!("Could not find WebView ({webview_id:?}) for WebDriver event: {input_event:?}"); - }; + } + + true } - fn handle_webdriver_screenshot( + pub(crate) fn maybe_window_for_webview_id( &self, webview_id: WebViewId, - rect: Option>, - result_sender: Sender>, - ) { - if let Some(webview) = self.webview_by_id(webview_id) { - let rect = rect.map(|rect| rect.to_box2d().into()); - webview.take_screenshot(rect, move |result| { - if let Err(error) = result_sender.send(result) { - warn!("Failed to send response to TakeScreenshot: {error}"); - } - }); - } else if let Err(error) = - result_sender.send(Err(ScreenshotCaptureError::WebViewDoesNotExist)) - { - error!("Failed to send response to TakeScreenshot: {error}"); + ) -> Option> { + for window in self.windows.borrow().values() { + if window.contains_webview(webview_id) { + return Some(window.clone()); + } } + None } - fn handle_webdriver_script_command(&self, script_command: &WebDriverScriptCommand) { - match script_command { - WebDriverScriptCommand::ExecuteScript(_webview_id, response_sender) | - WebDriverScriptCommand::ExecuteAsyncScript(_webview_id, response_sender) => { - // Give embedder a chance to interrupt the script command. - // Webdriver only handles 1 script command at a time, so we can - // safely set a new interrupt sender and remove the previous one here. - self.set_script_command_interrupt_sender(Some(response_sender.clone())); - }, - WebDriverScriptCommand::AddLoadStatusSender(webview_id, load_status_sender) => { - self.set_load_status_sender(*webview_id, load_status_sender.clone()); - }, - WebDriverScriptCommand::RemoveLoadStatusSender(webview_id) => { - self.remove_load_status_sender(*webview_id); - }, - _ => { - self.set_script_command_interrupt_sender(None); - }, - } + pub(crate) fn window_for_webview_id(&self, webview_id: WebViewId) -> Rc { + self.maybe_window_for_webview_id(webview_id) + .expect("Looking for unexpected WebView: {webview_id:?}") } - fn handle_webdriver_load_url( + pub(crate) fn platform_window_for_webview_id( &self, webview_id: WebViewId, - url: Url, - load_status_sender: GenericSender, - ) { - let Some(webview) = self.webview_by_id(webview_id) else { - return; - }; - - self.dismiss_embedder_controls_for_webview(webview_id); - - info!("Loading URL in webview {}: {}", webview_id, url); - self.set_load_status_sender(webview_id, load_status_sender); - webview.load(url); + ) -> Rc { + self.window_for_webview_id(webview_id).platform_window() } /// If we are exiting after achieving a stable image or we want to save the display of the /// [`WebView`] to an image file, request a screenshot of the [`WebView`]. fn maybe_request_screenshot(&self, webview: WebView) { - let output_path = self.servoshell_preferences().output_image_path.clone(); - if !self.servoshell_preferences().exit_after_stable_image && output_path.is_none() { + let output_path = self.servoshell_preferences.output_image_path.clone(); + if !self.servoshell_preferences.exit_after_stable_image && output_path.is_none() { return; } // Never request more than a single screenshot for now. - let achieved_stable_image = self.base().achieved_stable_image.clone(); + let achieved_stable_image = self.achieved_stable_image.clone(); if achieved_stable_image.get() { return; } @@ -382,4 +342,387 @@ pub trait RunningAppStateTrait { } }); } + + pub(crate) fn set_pending_traversal( + &self, + traversal_id: TraversalId, + sender: GenericSender, + ) { + self.webdriver_senders + .borrow_mut() + .pending_traversals + .insert(traversal_id, sender); + } + + pub(crate) fn set_load_status_sender( + &self, + webview_id: WebViewId, + sender: GenericSender, + ) { + self.webdriver_senders + .borrow_mut() + .load_status_senders + .insert(webview_id, sender); + } + + fn remove_load_status_sender(&self, webview_id: WebViewId) { + self.webdriver_senders + .borrow_mut() + .load_status_senders + .remove(&webview_id); + } + + fn set_script_command_interrupt_sender(&self, sender: Option>) { + self.webdriver_senders + .borrow_mut() + .script_evaluation_interrupt_sender = sender; + } + + pub(crate) fn handle_webdriver_input_event( + &self, + webview_id: WebViewId, + input_event: InputEvent, + response_sender: Option>, + ) { + if let Some(webview) = self.webview_by_id(webview_id) { + let event_id = webview.notify_input_event(input_event); + if let Some(response_sender) = response_sender { + self.pending_webdriver_events + .borrow_mut() + .insert(event_id, response_sender); + } + } else { + error!("Could not find WebView ({webview_id:?}) for WebDriver event: {input_event:?}"); + }; + } + + pub(crate) fn handle_webdriver_screenshot( + &self, + webview_id: WebViewId, + rect: Option>, + result_sender: Sender>, + ) { + if let Some(webview) = self.webview_by_id(webview_id) { + let rect = rect.map(|rect| rect.to_box2d().into()); + webview.take_screenshot(rect, move |result| { + if let Err(error) = result_sender.send(result) { + warn!("Failed to send response to TakeScreenshot: {error}"); + } + }); + } else if let Err(error) = + result_sender.send(Err(ScreenshotCaptureError::WebViewDoesNotExist)) + { + error!("Failed to send response to TakeScreenshot: {error}"); + } + } + + pub(crate) fn handle_webdriver_script_command(&self, script_command: &WebDriverScriptCommand) { + match script_command { + WebDriverScriptCommand::ExecuteScript(_webview_id, response_sender) | + WebDriverScriptCommand::ExecuteAsyncScript(_webview_id, response_sender) => { + // Give embedder a chance to interrupt the script command. + // Webdriver only handles 1 script command at a time, so we can + // safely set a new interrupt sender and remove the previous one here. + self.set_script_command_interrupt_sender(Some(response_sender.clone())); + }, + WebDriverScriptCommand::AddLoadStatusSender(webview_id, load_status_sender) => { + self.set_load_status_sender(*webview_id, load_status_sender.clone()); + }, + WebDriverScriptCommand::RemoveLoadStatusSender(webview_id) => { + self.remove_load_status_sender(*webview_id); + }, + _ => { + self.set_script_command_interrupt_sender(None); + }, + } + } + + pub(crate) fn handle_webdriver_load_url( + &self, + webview_id: WebViewId, + url: Url, + load_status_sender: GenericSender, + ) { + let Some(webview) = self.webview_by_id(webview_id) else { + return; + }; + + self.platform_window_for_webview_id(webview_id) + .dismiss_embedder_controls_for_webview(webview_id); + + info!("Loading URL in webview {}: {}", webview_id, url); + self.set_load_status_sender(webview_id, load_status_sender); + webview.load(url); + } + + pub(crate) fn handle_gamepad_events(&self) { + let Some(active_webview) = self + .focused_window() + .and_then(|window| window.focused_webview()) + else { + return; + }; + if let Some(gamepad_support) = self.gamepad_support.borrow_mut().as_mut() { + gamepad_support.handle_gamepad_events(active_webview); + } + } + + /// Interrupt any ongoing WebDriver-based script evaluation. + /// + /// From : + /// > The rules to execute a function body are as follows. The algorithm returns + /// > an ECMAScript completion record. + /// > + /// > If at any point during the algorithm a user prompt appears, immediately return + /// > Completion { Type: normal, Value: null, Target: empty }, but continue to run the + /// > other steps of this algorithm in parallel. + fn interrupt_webdriver_script_evaluation(&self) { + if let Some(sender) = &self + .webdriver_senders + .borrow() + .script_evaluation_interrupt_sender + { + sender.send(Ok(JSValue::Null)).unwrap_or_else(|err| { + info!( + "Notify dialog appear failed. Maybe the channel to webdriver is closed: {err}" + ); + }); + } + } +} + +impl WebViewDelegate for RunningAppState { + fn screen_geometry(&self, webview: WebView) -> Option { + Some( + self.platform_window_for_webview_id(webview.id()) + .screen_geometry(), + ) + } + + fn notify_status_text_changed(&self, webview: WebView, _status: Option) { + self.window_for_webview_id(webview.id()).set_needs_update(); + } + + fn notify_history_changed(&self, webview: WebView, _entries: Vec, _current: usize) { + self.window_for_webview_id(webview.id()).set_needs_update(); + } + + fn notify_page_title_changed(&self, webview: WebView, _: Option) { + self.window_for_webview_id(webview.id()).set_needs_update(); + } + + fn notify_traversal_complete(&self, _webview: WebView, traversal_id: TraversalId) { + let mut webdriver_state = self.webdriver_senders.borrow_mut(); + if let Entry::Occupied(entry) = webdriver_state.pending_traversals.entry(traversal_id) { + let sender = entry.remove(); + let _ = sender.send(WebDriverLoadStatus::Complete); + } + } + + fn request_move_to(&self, webview: WebView, new_position: DeviceIntPoint) { + self.platform_window_for_webview_id(webview.id()) + .set_position(new_position); + } + + fn request_resize_to(&self, webview: WebView, requested_outer_size: DeviceIntSize) { + self.platform_window_for_webview_id(webview.id()) + .request_resize(&webview, requested_outer_size); + } + + fn request_authentication( + &self, + webview: WebView, + authentication_request: AuthenticationRequest, + ) { + self.platform_window_for_webview_id(webview.id()) + .show_http_authentication_dialog(webview.id(), authentication_request); + } + + fn request_open_auxiliary_webview(&self, parent_webview: WebView) -> Option { + let window = self.window_for_webview_id(parent_webview.id()); + let platform_window = window.platform_window(); + + let webview = + WebViewBuilder::new_auxiliary(&self.servo, platform_window.rendering_context()) + .hidpi_scale_factor(platform_window.hidpi_scale_factor()) + .delegate(parent_webview.delegate()) + .build(); + + webview.notify_theme_change(platform_window.theme()); + + // When WebDriver is enabled, do not focus and raise the WebView to the top, + // as that is what the specification expects. Otherwise, we would like `window.open()` + // to create a new foreground tab + if self.servoshell_preferences.webdriver_port.is_none() { + webview.focus_and_raise_to_top(true); + } + + window.add_webview(webview.clone()); + + Some(webview) + } + + fn notify_closed(&self, webview: WebView) { + self.window_for_webview_id(webview.id()) + .close_webview(webview.id()) + } + + fn notify_focus_changed(&self, webview: WebView, focused: bool) { + self.window_for_webview_id(webview.id()) + .notify_focus_changed(webview, focused); + } + + fn notify_input_event_handled( + &self, + webview: WebView, + id: InputEventId, + result: InputEventResult, + ) { + self.platform_window_for_webview_id(webview.id()) + .notify_input_event_handled(&webview, id, result); + if let Some(response_sender) = self.pending_webdriver_events.borrow_mut().remove(&id) { + let _ = response_sender.send(()); + } + } + + fn notify_cursor_changed(&self, webview: WebView, cursor: servo::Cursor) { + self.platform_window_for_webview_id(webview.id()) + .set_cursor(cursor); + } + + fn notify_load_status_changed(&self, webview: WebView, status: LoadStatus) { + self.window_for_webview_id(webview.id()).set_needs_update(); + + if status == LoadStatus::Complete { + if let Some(sender) = self + .webdriver_senders + .borrow_mut() + .load_status_senders + .remove(&webview.id()) + { + let _ = sender.send(WebDriverLoadStatus::Complete); + } + self.maybe_request_screenshot(webview); + } + } + + fn notify_fullscreen_state_changed(&self, webview: WebView, fullscreen_state: bool) { + self.platform_window_for_webview_id(webview.id()) + .set_fullscreen(fullscreen_state); + } + + fn show_bluetooth_device_dialog( + &self, + webview: WebView, + devices: Vec, + response_sender: GenericSender>, + ) { + self.platform_window_for_webview_id(webview.id()) + .show_bluetooth_device_dialog(webview.id(), devices, response_sender); + } + + fn request_permission(&self, webview: WebView, permission_request: PermissionRequest) { + self.platform_window_for_webview_id(webview.id()) + .show_permission_dialog(webview.id(), permission_request); + } + + fn notify_new_frame_ready(&self, webview: WebView) { + self.window_for_webview_id(webview.id()).set_needs_repaint(); + } + + fn play_gamepad_haptic_effect( + &self, + _webview: WebView, + index: usize, + effect_type: GamepadHapticEffectType, + effect_complete_sender: IpcSender, + ) { + match self.gamepad_support.borrow_mut().as_mut() { + Some(gamepad_support) => { + gamepad_support.play_haptic_effect(index, effect_type, effect_complete_sender); + }, + None => { + let _ = effect_complete_sender.send(false); + }, + } + } + + fn stop_gamepad_haptic_effect( + &self, + _webview: WebView, + index: usize, + haptic_stop_sender: IpcSender, + ) { + let stopped = match self.gamepad_support.borrow_mut().as_mut() { + Some(gamepad_support) => gamepad_support.stop_haptic_effect(index), + None => false, + }; + let _ = haptic_stop_sender.send(stopped); + } + + fn show_embedder_control(&self, webview: WebView, embedder_control: EmbedderControl) { + if self.servoshell_preferences.webdriver_port.is_some() { + if matches!(&embedder_control, EmbedderControl::SimpleDialog(..)) { + self.interrupt_webdriver_script_evaluation(); + + // Dialogs block the page load, so need need to notify WebDriver + if let Some(sender) = self + .webdriver_senders + .borrow_mut() + .load_status_senders + .get(&webview.id()) + { + let _ = sender.send(WebDriverLoadStatus::Blocked); + }; + } + + self.webdriver_embedder_controls + .show_embedder_control(webview.id(), embedder_control); + return; + } + + self.window_for_webview_id(webview.id()) + .show_embedder_control(webview, embedder_control); + } + + fn hide_embedder_control(&self, webview: WebView, embedder_control_id: EmbedderControlId) { + if self.servoshell_preferences.webdriver_port.is_some() { + self.webdriver_embedder_controls + .hide_embedder_control(webview.id(), embedder_control_id); + return; + } + + self.window_for_webview_id(webview.id()) + .hide_embedder_control(webview, embedder_control_id); + } + + fn notify_favicon_changed(&self, webview: WebView) { + self.window_for_webview_id(webview.id()) + .notify_favicon_changed(webview); + } + + fn notify_media_session_event(&self, webview: WebView, event: MediaSessionEvent) { + self.platform_window_for_webview_id(webview.id()) + .notify_media_session_event(event); + } + + fn notify_crashed(&self, webview: WebView, reason: String, backtrace: Option) { + self.platform_window_for_webview_id(webview.id()) + .notify_crashed(webview, reason, backtrace); + } +} + +struct ServoShellServoDelegate; +impl ServoDelegate for ServoShellServoDelegate { + fn notify_devtools_server_started(&self, _servo: &Servo, port: u16, _token: String) { + info!("Devtools Server running on port {port}"); + } + + fn request_devtools_connection(&self, _servo: &Servo, request: AllowOrDenyRequest) { + request.allow(); + } + + fn notify_error(&self, _servo: &Servo, error: ServoError) { + error!("Saw Servo error: {error:?}!"); + } } diff --git a/ports/servoshell/webdriver.rs b/ports/servoshell/webdriver.rs new file mode 100644 index 00000000000..efd9f5e8dc3 --- /dev/null +++ b/ports/servoshell/webdriver.rs @@ -0,0 +1,308 @@ +/* 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::RefCell; +use std::collections::HashMap; +use std::rc::Rc; + +use log::warn; +use servo::{ + EmbedderControl, EmbedderControlId, SimpleDialog, WebDriverCommandMsg, WebDriverUserPrompt, + WebDriverUserPromptAction, WebViewId, +}; +use url::Url; + +use crate::running_app_state::RunningAppState; + +#[derive(Default)] +pub(crate) struct WebDriverEmbedderControls { + embedder_controls: RefCell>>, +} + +impl WebDriverEmbedderControls { + pub(crate) fn show_embedder_control( + &self, + webview_id: WebViewId, + embedder_control: EmbedderControl, + ) { + self.embedder_controls + .borrow_mut() + .entry(webview_id) + .or_default() + .push(embedder_control) + } + + pub(crate) fn hide_embedder_control( + &self, + webview_id: WebViewId, + embedder_control_id: EmbedderControlId, + ) { + let mut embedder_controls = self.embedder_controls.borrow_mut(); + if let Some(controls) = embedder_controls.get_mut(&webview_id) { + controls.retain(|control| control.id() != embedder_control_id); + } + embedder_controls.retain(|_, controls| !controls.is_empty()); + } + + pub(crate) fn current_active_dialog_webdriver_type( + &self, + webview_id: WebViewId, + ) -> Option { + // From + // > Step 3: If the current user prompt is an alert dialog, set type to "alert". Otherwise, + // > if the current user prompt is a beforeunload dialog, set type to + // > "beforeUnload". Otherwise, if the current user prompt is a confirm dialog, set + // > type to "confirm". Otherwise, if the current user prompt is a prompt dialog, + // > set type to "prompt". + let embedder_controls = self.embedder_controls.borrow(); + match embedder_controls.get(&webview_id)?.last()? { + EmbedderControl::SimpleDialog(SimpleDialog::Alert { .. }) => { + Some(WebDriverUserPrompt::Alert) + }, + EmbedderControl::SimpleDialog(SimpleDialog::Confirm { .. }) => { + Some(WebDriverUserPrompt::Confirm) + }, + EmbedderControl::SimpleDialog(SimpleDialog::Prompt { .. }) => { + Some(WebDriverUserPrompt::Prompt) + }, + EmbedderControl::FilePicker { .. } => Some(WebDriverUserPrompt::File), + EmbedderControl::SelectElement { .. } => Some(WebDriverUserPrompt::Default), + _ => None, + } + } + + /// Respond to the most recently added dialog if it was a `SimpleDialog` and return + /// its message string or return an error if there is no active dialog or the most + /// recently added dialog is not a `SimpleDialog`. + pub(crate) fn respond_to_active_simple_dialog( + &self, + webview_id: WebViewId, + action: WebDriverUserPromptAction, + ) -> Result { + let mut embedder_controls = self.embedder_controls.borrow_mut(); + let Some(controls) = embedder_controls.get_mut(&webview_id) else { + return Err(()); + }; + let Some(last_control) = controls.last() else { + return Err(()); + }; + let EmbedderControl::SimpleDialog(simple_dialog) = last_control else { + return Err(()); + }; + + let result_text = simple_dialog.message().to_owned(); + match action { + WebDriverUserPromptAction::Accept => simple_dialog.accept(), + WebDriverUserPromptAction::Dismiss => simple_dialog.dismiss(), + WebDriverUserPromptAction::Ignore => return Ok(result_text), + }; + + controls.pop(); + Ok(result_text) + } + + pub(crate) fn alert_text_of_newest_dialog(&self, webview_id: WebViewId) -> Option { + let embedder_controls = self.embedder_controls.borrow(); + match embedder_controls.get(&webview_id)?.last()? { + EmbedderControl::SimpleDialog(simple_dialog) => { + Some(simple_dialog.message().to_owned()) + }, + _ => None, + } + } + + pub(crate) fn set_alert_text_of_newest_dialog(&self, webview_id: WebViewId, text: String) { + let mut embedder_controls = self.embedder_controls.borrow_mut(); + let Some(controls) = embedder_controls.get_mut(&webview_id) else { + return; + }; + let Some(last_control) = controls.last_mut() else { + return; + }; + // FIXME: This should be setting the prompt result of the dialog and not the + // message text according to the WebDriver specification at: + // + if let EmbedderControl::SimpleDialog(simple_dialog) = last_control { + simple_dialog.set_message(text) + } + } +} + +impl RunningAppState { + pub(crate) fn handle_webdriver_messages(self: &Rc) { + let Some(webdriver_receiver) = self.webdriver_receiver() else { + return; + }; + + while let Ok(msg) = webdriver_receiver.try_recv() { + match msg { + WebDriverCommandMsg::Shutdown => { + self.servo().start_shutting_down(); + }, + WebDriverCommandMsg::IsWebViewOpen(webview_id, sender) => { + let context = self.webview_by_id(webview_id); + + if let Err(error) = sender.send(context.is_some()) { + warn!("Failed to send response of IsWebViewOpen: {error}"); + } + }, + WebDriverCommandMsg::IsBrowsingContextOpen(..) => { + self.servo().execute_webdriver_command(msg); + }, + WebDriverCommandMsg::NewWebView(response_sender, load_status_sender) => { + let new_webview = self + .any_window() + .create_toplevel_webview(self.clone(), Url::parse("about:blank").unwrap()); + + if let Err(error) = response_sender.send(new_webview.id()) { + warn!("Failed to send response of NewWebview: {error}"); + } + if let Some(load_status_sender) = load_status_sender { + self.set_load_status_sender(new_webview.id(), load_status_sender); + } + }, + WebDriverCommandMsg::CloseWebView(webview_id, response_sender) => { + self.window_for_webview_id(webview_id) + .close_webview(webview_id); + if let Err(error) = response_sender.send(()) { + warn!("Failed to send response of CloseWebView: {error}"); + } + }, + WebDriverCommandMsg::FocusWebView(webview_id) => { + if let Some(webview) = self.webview_by_id(webview_id) { + webview.focus_and_raise_to_top(true); + } + }, + WebDriverCommandMsg::FocusBrowsingContext(..) => { + self.servo().execute_webdriver_command(msg); + }, + WebDriverCommandMsg::GetAllWebViews(response_sender) => { + let webviews = self + .windows() + .values() + .flat_map(|window| window.webview_ids()) + .collect(); + if let Err(error) = response_sender.send(webviews) { + warn!("Failed to send response of GetAllWebViews: {error}"); + } + }, + WebDriverCommandMsg::GetWindowRect(webview_id, response_sender) => { + let platform_window = self.platform_window_for_webview_id(webview_id); + if let Err(error) = response_sender.send(platform_window.window_rect()) { + warn!("Failed to send response of GetWindowSize: {error}"); + } + }, + WebDriverCommandMsg::MaximizeWebView(webview_id, response_sender) => { + let Some(webview) = self.webview_by_id(webview_id) else { + continue; + }; + let platform_window = self.platform_window_for_webview_id(webview_id); + platform_window.maximize(&webview); + + if let Err(error) = response_sender.send(platform_window.window_rect()) { + warn!("Failed to send response of GetWindowSize: {error}"); + } + }, + WebDriverCommandMsg::SetWindowRect(webview_id, requested_rect, size_sender) => { + let Some(webview) = self.webview_by_id(webview_id) else { + continue; + }; + + let platform_window = self.platform_window_for_webview_id(webview_id); + let scale = platform_window.hidpi_scale_factor(); + + let requested_physical_rect = + (requested_rect.to_f32() * scale).round().to_i32(); + + // Step 17. Set Width/Height. + platform_window.request_resize(&webview, requested_physical_rect.size()); + + // Step 18. Set position of the window. + platform_window.set_position(requested_physical_rect.min); + + if let Err(error) = size_sender.send(platform_window.window_rect()) { + warn!("Failed to send window size: {error}"); + } + }, + WebDriverCommandMsg::GetViewportSize(webview_id, response_sender) => { + let platform_window = self.platform_window_for_webview_id(webview_id); + let size = platform_window.rendering_context().size2d(); + if let Err(error) = response_sender.send(size) { + warn!("Failed to send response of GetViewportSize: {error}"); + } + }, + // This is only received when start new session. + WebDriverCommandMsg::GetFocusedWebView(sender) => { + let focused_webview = self.any_window().focused_webview(); + if let Err(error) = sender.send(focused_webview.map(|w| w.id())) { + warn!("Failed to send response of GetFocusedWebView: {error}"); + }; + }, + WebDriverCommandMsg::LoadUrl(webview_id, url, load_status_sender) => { + self.handle_webdriver_load_url(webview_id, url, load_status_sender); + }, + WebDriverCommandMsg::Refresh(webview_id, load_status_sender) => { + if let Some(webview) = self.webview_by_id(webview_id) { + self.set_load_status_sender(webview_id, load_status_sender); + webview.reload(); + } + }, + WebDriverCommandMsg::GoBack(webview_id, load_status_sender) => { + if let Some(webview) = self.webview_by_id(webview_id) { + let traversal_id = webview.go_back(1); + self.set_pending_traversal(traversal_id, load_status_sender); + } + }, + WebDriverCommandMsg::GoForward(webview_id, load_status_sender) => { + if let Some(webview) = self.webview_by_id(webview_id) { + let traversal_id = webview.go_forward(1); + self.set_pending_traversal(traversal_id, load_status_sender); + } + }, + WebDriverCommandMsg::InputEvent(webview_id, input_event, response_sender) => { + self.handle_webdriver_input_event(webview_id, input_event, response_sender); + }, + WebDriverCommandMsg::ScriptCommand(_, ref webdriver_script_command) => { + self.handle_webdriver_script_command(webdriver_script_command); + self.servo().execute_webdriver_command(msg); + }, + WebDriverCommandMsg::CurrentUserPrompt(webview_id, response_sender) => { + let current_dialog = self + .webdriver_embedder_controls + .current_active_dialog_webdriver_type(webview_id); + if let Err(error) = response_sender.send(current_dialog) { + warn!("Failed to send response of CurrentUserPrompt: {error}"); + }; + }, + WebDriverCommandMsg::HandleUserPrompt(webview_id, action, response_sender) => { + let controls = &self.webdriver_embedder_controls; + let result = controls.respond_to_active_simple_dialog(webview_id, action); + if let Err(error) = response_sender.send(result) { + warn!("Failed to send response of HandleUserPrompt: {error}"); + }; + }, + WebDriverCommandMsg::GetAlertText(webview_id, response_sender) => { + let response = match self + .webdriver_embedder_controls + .alert_text_of_newest_dialog(webview_id) + { + Some(text) => Ok(text), + None => Err(()), + }; + + if let Err(error) = response_sender.send(response) { + warn!("Failed to send response of GetAlertText: {error}"); + }; + }, + WebDriverCommandMsg::SendAlertText(webview_id, text) => { + self.webdriver_embedder_controls + .set_alert_text_of_newest_dialog(webview_id, text); + }, + WebDriverCommandMsg::TakeScreenshot(webview_id, rect, result_sender) => { + self.handle_webdriver_screenshot(webview_id, rect, result_sender); + }, + }; + } + } +} diff --git a/ports/servoshell/window.rs b/ports/servoshell/window.rs new file mode 100644 index 00000000000..86a3fb2df2d --- /dev/null +++ b/ports/servoshell/window.rs @@ -0,0 +1,373 @@ +/* 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 servo::base::generic_channel::GenericSender; +use servo::servo_geometry::{DeviceIndependentIntRect, DeviceIndependentPixel}; +use servo::webrender_api::units::{DeviceIntPoint, DeviceIntSize, DevicePixel}; +use servo::{ + AuthenticationRequest, Cursor, EmbedderControl, EmbedderControlId, InputEventId, + InputEventResult, MediaSessionEvent, PermissionRequest, RenderingContext, ScreenGeometry, + WebView, WebViewBuilder, WebViewId, +}; +use url::Url; + +use crate::running_app_state::{RunningAppState, 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>, +} + +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(), + } + } + + pub(crate) fn id(&self) -> ServoShellWindowId { + self.platform_window().id() + } + + pub(crate) fn create_and_focus_toplevel_webview( + &self, + state: Rc, + url: Url, + ) -> WebView { + let webview = self.create_toplevel_webview(state, url); + webview.focus_and_raise_to_top(true); + webview + } + + pub(crate) fn create_toplevel_webview(&self, state: Rc, url: Url) -> WebView { + let webview = WebViewBuilder::new(state.servo(), self.platform_window.rendering_context()) + .url(url) + .hidpi_scale_factor(self.platform_window.hidpi_scale_factor()) + .delegate(state.clone()) + .build(); + + webview.notify_theme_change(self.platform_window.theme()); + self.add_webview(webview.clone()); + webview + } + + /// Repaint the focused [`WebView`]. + pub(crate) fn repaint_webviews(&self) { + let Some(webview) = self.focused_webview() else { + return; + }; + + self.platform_window() + .rendering_context() + .make_current() + .unwrap(); + 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) + } + + 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 focused(&self) -> bool { + self.platform_window.focused() + } + + pub(crate) fn add_webview(&self, webview: WebView) { + self.webview_collection.borrow_mut().add(webview); + 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() + } + + #[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))] + pub(crate) fn focus_webview_by_index(&self, index: usize) { + if let Some((_, webview)) = self.webviews().get(index) { + webview.focus(); + } + } + + #[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))] + pub(crate) fn get_focused_webview_index(&self) -> Option { + let focused_id = self.webview_collection.borrow().focused_id()?; + self.webviews() + .iter() + .position(|webview| webview.0 == focused_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 compositor 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); + + if let Some(newest_webview) = webview_collection.newest() { + newest_webview.focus(); + } + } + + pub(crate) fn notify_focus_changed(&self, webview: WebView, focused: bool) { + let mut webview_collection = self.webview_collection.borrow_mut(); + if focused { + webview.show(true); + self.set_needs_update(); + webview_collection.set_focused(Some(webview.id())); + } else if webview_collection.focused_id() == Some(webview.id()) { + webview_collection.set_focused(None); + } + } + + 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 focused_webview(&self) -> Option { + self.webview_collection.borrow().focused().cloned() + } + + #[cfg_attr( + not(any(target_os = "android", target_env = "ohos")), + expect(dead_code) + )] + pub(crate) fn focused_or_newest_webview(&self) -> Option { + let webview_collection = self.webview_collection.borrow(); + webview_collection + .focused() + .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(); + } +} + +/// 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. + 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 + } + /// Handle a winit [`WindowEvent`]. Returns `true` if the event loop should continue + /// and `false` otherwise. + /// + /// TODO: This should be handled internally in the winit window if possible so that it + /// makes more sense when we are mixing headed and headless windows. + #[cfg(not(any(target_os = "android", target_env = "ohos")))] + fn handle_winit_window_event( + &self, + _: Rc, + _: &ServoShellWindow, + _: winit::event::WindowEvent, + ) { + } + /// Handle a winit [`AppEvent`]. Returns `true` if the event loop should continue and + /// `false` otherwise. + /// + /// TODO: This should be handled internally in the winit window if possible so that it + /// makes more sense when we are mixing headed and headless windows. + #[cfg(not(any(target_os = "android", target_env = "ohos")))] + fn handle_winit_app_event( + &self, + _: Rc, + _: &ServoShellWindow, + _: crate::desktop::event_loop::AppEvent, + ) { + } + /// 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 focused(&self) -> bool; + + 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, + _devices: Vec, + _: GenericSender>, + ) { + } + 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) {} +} diff --git a/support/android/apk/servoapp/src/main/java/org/servo/servoshell/MainActivity.java b/support/android/apk/servoapp/src/main/java/org/servo/servoshell/MainActivity.java index c63a66b9f03..71ef0e39bfc 100644 --- a/support/android/apk/servoapp/src/main/java/org/servo/servoshell/MainActivity.java +++ b/support/android/apk/servoapp/src/main/java/org/servo/servoshell/MainActivity.java @@ -232,19 +232,6 @@ public class MainActivity extends Activity implements Servo.Client { mCanGoBack = canGoBack; } - @Override - public boolean onAllowNavigation(String url) { - if (url.startsWith("market://")) { - try { - startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url))); - return false; - } catch (Exception e) { - Log.e("onAllowNavigation", e.toString()); - } - } - return true; - } - public void onRedrawing(boolean redrawing) { if (redrawing) { mIdleText.setText("LOOP"); diff --git a/support/android/apk/servoview/src/main/java/org/servo/servoview/Servo.java b/support/android/apk/servoview/src/main/java/org/servo/servoview/Servo.java index aa444f1c2c3..ba0a47673f1 100644 --- a/support/android/apk/servoview/src/main/java/org/servo/servoview/Servo.java +++ b/support/android/apk/servoview/src/main/java/org/servo/servoview/Servo.java @@ -178,8 +178,6 @@ public class Servo { public interface Client { void onAlert(String message); - boolean onAllowNavigation(String url); - void onLoadStarted(); void onLoadEnded(); @@ -260,10 +258,6 @@ public class Servo { mRunCallback.inUIThread(() -> mClient.onImeHide()); } - public boolean onAllowNavigation(String url) { - return mClient.onAllowNavigation(url); - } - public void onLoadStarted() { mRunCallback.inUIThread(() -> mClient.onLoadStarted()); }