From 5114e24db145b329ca7945c1d243fe1c6817e500 Mon Sep 17 00:00:00 2001 From: Narfinger Date: Wed, 11 Jun 2025 10:51:44 +0200 Subject: [PATCH] Servoshell: Refactor save_output_image and implement into OHOS (#37237) Split out `save_output_image_if_necessary` into its own file so the code can be shared by servoshell on the desktop and ohos. Additionally, hook it up to the loop in OHOS and have OHOS allow exiting. Testing: Manual testing on ohos and desktop. --------- Signed-off-by: Narfinger --- ports/servoshell/desktop/app_state.rs | 36 ++++------------- ports/servoshell/egl/app_state.rs | 9 ++++- ports/servoshell/egl/ohos.rs | 19 ++++++++- ports/servoshell/lib.rs | 1 + ports/servoshell/output_image.rs | 39 +++++++++++++++++++ .../entry/src/main/ets/pages/Index.ets | 2 + 6 files changed, 75 insertions(+), 31 deletions(-) create mode 100644 ports/servoshell/output_image.rs diff --git a/ports/servoshell/desktop/app_state.rs b/ports/servoshell/desktop/app_state.rs index ce6cdb7beb2..0c16e09a2c3 100644 --- a/ports/servoshell/desktop/app_state.rs +++ b/ports/servoshell/desktop/app_state.rs @@ -7,15 +7,14 @@ use std::collections::HashMap; use std::path::PathBuf; use std::rc::Rc; -use euclid::{Point2D, Vector2D}; -use image::{DynamicImage, ImageFormat}; +use euclid::Vector2D; use keyboard_types::{Key, KeyboardEvent, Modifiers, ShortcutMatcher}; use log::{error, info}; use servo::base::id::WebViewId; use servo::config::{opts, pref}; use servo::ipc_channel::ipc::IpcSender; use servo::webrender_api::ScrollLocation; -use servo::webrender_api::units::{DeviceIntPoint, DeviceIntRect, DeviceIntSize}; +use servo::webrender_api::units::{DeviceIntPoint, DeviceIntSize}; use servo::{ AllowOrDenyRequest, AuthenticationRequest, FilterPattern, FormControl, GamepadHapticEffectType, LoadStatus, PermissionRequest, Servo, ServoDelegate, ServoError, SimpleDialog, TouchEventType, @@ -28,6 +27,7 @@ use super::dialog::Dialog; use super::gamepad::GamepadSupport; use super::keyutils::CMD_OR_CONTROL; use super::window_trait::{LINE_HEIGHT, WindowPortsMethods}; +use crate::output_image::save_output_image_if_necessary; use crate::prefs::ServoShellPreferences; pub(crate) enum AppState { @@ -140,31 +140,6 @@ impl RunningAppState { } } - pub(crate) fn save_output_image_if_necessary(&self) { - let Some(output_path) = self.servoshell_preferences.output_image_path.as_ref() else { - return; - }; - - let inner = self.inner(); - let size = inner.window.rendering_context().size2d().to_i32(); - let viewport_rect = DeviceIntRect::from_origin_and_size(Point2D::origin(), size); - let Some(image) = inner - .window - .rendering_context() - .read_to_image(viewport_rect) - else { - error!("Failed to read output image."); - return; - }; - - let image_format = ImageFormat::from_path(output_path).unwrap_or(ImageFormat::Png); - if let Err(error) = - DynamicImage::ImageRgba8(image).save_with_format(output_path, image_format) - { - error!("Failed to save {output_path}: {error}."); - } - } - /// 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. @@ -181,7 +156,10 @@ impl RunningAppState { // This needs to be done before presenting(), because `ReneringContext::read_to_image` reads // from the back buffer. - self.save_output_image_if_necessary(); + save_output_image_if_necessary( + &self.servoshell_preferences, + &self.inner().window.rendering_context(), + ); let mut inner_mut = self.inner_mut(); inner_mut.window.rendering_context().present(); diff --git a/ports/servoshell/egl/app_state.rs b/ports/servoshell/egl/app_state.rs index 114c9c5f4d0..cb57147f52d 100644 --- a/ports/servoshell/egl/app_state.rs +++ b/ports/servoshell/egl/app_state.rs @@ -25,6 +25,7 @@ use servo::{ use url::Url; use crate::egl::host_trait::HostTrait; +use crate::output_image::save_output_image_if_necessary; use crate::prefs::ServoShellPreferences; #[derive(Clone, Debug)] @@ -718,8 +719,14 @@ impl RunningAppState { pub fn present_if_needed(&self) { if self.inner().need_present { self.inner_mut().need_present = false; - self.active_webview().paint(); + if !self.active_webview().paint() { + return; + } + save_output_image_if_necessary(&self.servoshell_preferences, &self.rendering_context); self.rendering_context.present(); + if self.servoshell_preferences.exit_after_stable_image { + self.request_shutdown(); + } } } } diff --git a/ports/servoshell/egl/ohos.rs b/ports/servoshell/egl/ohos.rs index a1b76758a9a..8297cf77cb8 100644 --- a/ports/servoshell/egl/ohos.rs +++ b/ports/servoshell/egl/ohos.rs @@ -123,6 +123,8 @@ const PROMPT_QUEUE_SIZE: usize = 4; static SET_URL_BAR_CB: OnceLock< ThreadsafeFunction, > = OnceLock::new(); +static TERMINATE_CALLBACK: OnceLock> = + OnceLock::new(); static PROMPT_TOAST: OnceLock< ThreadsafeFunction, > = OnceLock::new(); @@ -601,6 +603,15 @@ pub fn register_url_callback(callback: Function) -> napi_ohos::Resul }) } +#[napi(js_name = "registerTerminateCallback")] +pub fn register_terminate_callback(callback: Function<(), ()>) -> napi_ohos::Result<()> { + let tsfn_builder = callback.build_threadsafe_function(); + let function = tsfn_builder.max_queue_size::<1>().build()?; + TERMINATE_CALLBACK + .set(function) + .map_err(|_| napi_ohos::Error::from_reason("Failed to set terminate function")) +} + #[napi] pub fn register_prompt_toast_callback(callback: Function) -> napi_ohos::Result<()> { debug!("register_prompt_toast_callback called!"); @@ -850,7 +861,13 @@ impl HostTrait for HostCallbacks { // todo: should we tell the vsync thread that it should perform updates? } - fn on_shutdown_complete(&self) {} + fn on_shutdown_complete(&self) { + if let Some(terminate_fn) = TERMINATE_CALLBACK.get() { + terminate_fn.call((), ThreadsafeFunctionCallMode::Blocking); + } else { + error!("Could not shut down despite servo shutting down"); + } + } /// Shows the Inputmethod /// diff --git a/ports/servoshell/lib.rs b/ports/servoshell/lib.rs index 54b4d2d54a0..8f05b5b56b5 100644 --- a/ports/servoshell/lib.rs +++ b/ports/servoshell/lib.rs @@ -15,6 +15,7 @@ mod crash_handler; pub(crate) mod desktop; #[cfg(any(target_os = "android", target_env = "ohos"))] mod egl; +mod output_image; #[cfg(not(any(target_os = "android", target_env = "ohos")))] mod panic_hook; mod parser; diff --git a/ports/servoshell/output_image.rs b/ports/servoshell/output_image.rs new file mode 100644 index 00000000000..27f31f6795a --- /dev/null +++ b/ports/servoshell/output_image.rs @@ -0,0 +1,39 @@ +/* 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::rc::Rc; + +use euclid::Point2D; +use image::{DynamicImage, ImageFormat}; +use log::error; +use servo::RenderingContext; +use servo::webrender_api::units::DeviceIntRect; + +use crate::prefs::ServoShellPreferences; + +/// This needs to be done before presenting(), because `ReneringContext::read_to_image` reads +/// from the back buffer. This does nothing if the preference `output_image_path` is not set. +pub(crate) fn save_output_image_if_necessary( + prefs: &ServoShellPreferences, + rendering_context: &Rc, +) where + T: RenderingContext + ?Sized, +{ + let Some(output_path) = prefs.output_image_path.as_ref() else { + return; + }; + + let size = rendering_context.size2d().to_i32(); + let viewport_rect = DeviceIntRect::from_origin_and_size(Point2D::origin(), size); + let Some(image) = rendering_context.read_to_image(viewport_rect) else { + error!("Failed to read output image."); + return; + }; + + let image_format = ImageFormat::from_path(output_path).unwrap_or(ImageFormat::Png); + if let Err(error) = DynamicImage::ImageRgba8(image).save_with_format(output_path, image_format) + { + error!("Failed to save {output_path}: {error}."); + } +} diff --git a/support/openharmony/entry/src/main/ets/pages/Index.ets b/support/openharmony/entry/src/main/ets/pages/Index.ets index 9c6cb06537b..14eeb996ac5 100644 --- a/support/openharmony/entry/src/main/ets/pages/Index.ets +++ b/support/openharmony/entry/src/main/ets/pages/Index.ets @@ -8,6 +8,7 @@ interface ServoXComponentInterface { goBack(): void; goForward(): void; registerURLcallback(callback: (url: string) => void): void; + registerTerminateCallback(callback: () => void): void; registerPromptToastCallback(callback: (msg: string) => void): void initServo(options: InitOpts): void; } @@ -130,6 +131,7 @@ struct Index { console.info('New URL from native: ', new_url) this.urlToLoad = new_url }) + this.xComponentContext.registerTerminateCallback(() => { this.context?.terminateSelf(); }) this.xComponentContext.registerPromptToastCallback(prompt_toast) }) }