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 <Narfinger@users.noreply.github.com>
This commit is contained in:
Narfinger 2025-06-11 10:51:44 +02:00 committed by GitHub
parent f60c857fcf
commit 5114e24db1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 75 additions and 31 deletions

View file

@ -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();

View file

@ -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();
}
}
}
}

View file

@ -123,6 +123,8 @@ const PROMPT_QUEUE_SIZE: usize = 4;
static SET_URL_BAR_CB: OnceLock<
ThreadsafeFunction<String, (), String, false, false, UPDATE_URL_QUEUE_SIZE>,
> = OnceLock::new();
static TERMINATE_CALLBACK: OnceLock<ThreadsafeFunction<(), (), (), false, false, 1>> =
OnceLock::new();
static PROMPT_TOAST: OnceLock<
ThreadsafeFunction<String, (), String, false, false, PROMPT_QUEUE_SIZE>,
> = OnceLock::new();
@ -601,6 +603,15 @@ pub fn register_url_callback(callback: Function<String, ()>) -> 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<String, ()>) -> 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
///

View file

@ -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;

View file

@ -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<T>(
prefs: &ServoShellPreferences,
rendering_context: &Rc<T>,
) 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}.");
}
}

View file

@ -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)
})
}