servoshell: ohos / android: Decouple initialization and window creation (#41532)

We can start initializing servo without needing to wait for the
creation of a window.
This allows us to perform initialization of servo itself,
concurrently to the app UI thread setting up the app UI.
Once the native window is ready, we can then let servo create
the first window and load the initial URL.
This is also interesting in the context of using servo as a
webview library, where loading the library / initialising the
webview and loading the first url are typically decoupled
steps.

Note: on the android port the Java code is not touched, which means that
we effectively still
perform the initialization at the same point in time on android. 
The change to call the base servo initialiation from the Java app, can
be done in a seperate PR, perhaps by someone more familiar with android
than me.

Follow-up PRs will add multi-window support, which means that some of
the multi-webview related code in this PR has open todos,
which will be addressed by follow-ups.



Testing: The ohos-port is tested in CI, the android port was manually
tested by me.

---------

Signed-off-by: Jonathan Schwender <schwenderjonathan@gmail.com>
This commit is contained in:
Jonathan Schwender
2026-01-14 17:21:18 +01:00
committed by GitHub
parent e58bdc03f9
commit 66d8601c81
6 changed files with 262 additions and 233 deletions

View File

@@ -7,15 +7,13 @@
mod resources;
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 euclid::{Point2D, Rect, Scale, Size2D};
use jni::objects::{GlobalRef, JClass, JObject, JString, JValue, JValueOwned};
use jni::sys::{jboolean, jfloat, jint, jobject};
use jni::{JNIEnv, JavaVM};
@@ -26,13 +24,13 @@ use raw_window_handle::{
WindowHandle,
};
use resources::ResourceReaderInstance;
pub use servo::MediaSessionPlaybackState;
use servo::{
self, DevicePixel, EventLoopWaker, InputMethodControl, LoadStatus, MediaSessionActionType,
MouseButton, PrefValue,
};
pub use servo::{MediaSessionPlaybackState, WindowRenderingContext};
use super::app::{App, AppInitOptions, VsyncRefreshDriver};
use super::app::{App, AppInitOptions};
use super::host_trait::HostTrait;
use crate::prefs::{ArgumentParsingResult, EXPERIMENTAL_PREFS, parse_command_line_arguments};
@@ -100,8 +98,7 @@ pub extern "C" fn Java_org_servo_servoview_JNIServo_init<'local>(
callbacks_obj: JObject<'local>,
surface: JObject<'local>,
) {
let (mut init_opts, log, log_str, _gst_debug_str) = match get_options(&mut env, &opts, &surface)
{
let (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);
@@ -165,7 +162,7 @@ pub extern "C" fn Java_org_servo_servoview_JNIServo_init<'local>(
};
let event_loop_waker = Box::new(WakeupCallback::new(callbacks_ref.clone(), &env));
let host = Box::new(HostCallbacks::new(callbacks_ref, &env));
let host = Rc::new(HostCallbacks::new(callbacks_ref, &env));
crate::init_crypto();
servo::resources::set(Box::new(ResourceReaderInstance::new()));
@@ -195,33 +192,26 @@ pub extern "C" fn Java_org_servo_servoview_JNIServo_init<'local>(
)
};
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"),
);
let hidpi_scale_factor = Scale::new(init_opts.density);
APP.with(|app| {
*app.borrow_mut() = Some(App::new(AppInitOptions {
let new_app = 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,
}));
});
new_app.add_platform_window(
display_handle,
window_handle,
init_opts.viewport_rect,
hidpi_scale_factor,
);
*app.borrow_mut() = Some(new_app);
});
}

View File

@@ -9,7 +9,7 @@ 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 raw_window_handle::{DisplayHandle, RawWindowHandle, WindowHandle};
use servo::{
DeviceIndependentIntRect, DeviceIndependentPixel, DeviceIntSize, DevicePixel, DevicePoint,
DeviceVector2D, EmbedderControl, EmbedderControlId, EventLoopWaker, ImeEvent, InputEvent,
@@ -27,7 +27,7 @@ use crate::running_app_state::{RunningAppState, UserInterfaceCommand};
use crate::window::{PlatformWindow, ServoShellWindow, ServoShellWindowId};
pub(crate) struct EmbeddedPlatformWindow {
host: Box<dyn HostTrait>,
host: Rc<dyn HostTrait>,
rendering_context: Rc<WindowRenderingContext>,
refresh_driver: Rc<VsyncRefreshDriver>,
viewport_rect: RefCell<Rect<i32, DevicePixel>>,
@@ -247,12 +247,8 @@ impl RefreshDriver for VsyncRefreshDriver {
}
pub(crate) struct AppInitOptions {
pub host: Box<dyn HostTrait>,
pub host: Rc<dyn HostTrait>,
pub event_loop_waker: Box<dyn EventLoopWaker>,
pub viewport_rect: Rect<i32, DevicePixel>,
pub hidpi_scale_factor: f32,
pub rendering_context: Rc<WindowRenderingContext>,
pub refresh_driver: Rc<VsyncRefreshDriver>,
pub initial_url: Option<String>,
pub opts: Opts,
pub preferences: Preferences,
@@ -263,7 +259,11 @@ pub(crate) struct AppInitOptions {
pub struct App {
state: Rc<RunningAppState>,
platform_window: Rc<EmbeddedPlatformWindow>,
// TODO: multi-window support, like desktop version.
// This is just an intermediate state, to split refactoring into
// multiple PRs.
host: Rc<dyn HostTrait>,
initial_url: Url,
}
#[expect(unused)]
@@ -292,12 +292,37 @@ impl App {
user_content_manager,
));
let platform_window = Rc::new(EmbeddedPlatformWindow {
Rc::new(Self {
state,
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),
initial_url,
})
}
pub(crate) fn add_platform_window(
&self,
display_handle: DisplayHandle,
window_handle: WindowHandle,
viewport_rect: Rect<i32, DevicePixel>,
hidpi_scale_factor: Scale<f32, DeviceIndependentPixel, DevicePixel>,
) {
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"),
);
let platform_window = Rc::new(EmbeddedPlatformWindow {
host: self.host.clone(),
rendering_context,
refresh_driver,
viewport_rect: RefCell::new(viewport_rect),
hidpi_scale_factor,
visible_input_methods: Default::default(),
current_title: Default::default(),
current_url: Default::default(),
@@ -305,12 +330,8 @@ impl App {
current_can_go_forward: Default::default(),
current_load_status: Default::default(),
});
state.open_window(platform_window.clone(), initial_url);
Rc::new(Self {
state,
platform_window,
})
self.state
.open_window(platform_window.clone(), self.initial_url.clone());
}
pub(crate) fn servo(&self) -> &Servo {
@@ -334,6 +355,10 @@ impl App {
self.window().active_or_newest_webview()
}
pub(crate) fn initial_url(&self) -> Url {
self.initial_url.clone()
}
pub(crate) fn create_and_activate_toplevel_webview(self: &Rc<Self>, url: Url) -> WebView {
self.window()
.create_and_activate_toplevel_webview(self.state.clone(), url)
@@ -352,7 +377,7 @@ impl App {
.state
.spin_event_loop(None /* create_platform_window */)
{
self.platform_window.host.on_shutdown_complete();
self.host.on_shutdown_complete()
}
}
@@ -396,7 +421,10 @@ impl App {
let size = viewport_rect.size;
webview.resize(PhysicalSize::new(size.width as u32, size.height as u32));
}
*self.platform_window.viewport_rect.borrow_mut() = viewport_rect;
let window = self.window().platform_window();
let embedded_platform_window = window.as_headed_window().expect("No headed window");
*embedded_platform_window.viewport_rect.borrow_mut() = viewport_rect;
self.spin_event_loop();
}
@@ -587,14 +615,24 @@ impl App {
}
}
// TODO: Instead of letting the embedder drive the RefreshDriver we should move the vsync
// notification directly into the VsyncRefreshDriver.
pub fn notify_vsync(&self) {
self.platform_window.refresh_driver.notify_vsync();
let platform_window = self.window().platform_window();
let embedded_platform_window = platform_window
.as_headed_window()
.expect("No headed window");
embedded_platform_window.refresh_driver.notify_vsync();
self.spin_event_loop();
}
pub fn pause_painting(&self) {
if let Err(e) = self.platform_window.rendering_context.take_window() {
warn!("Unbinding native surface from context failed ({:?})", e);
let platform_window = self.window().platform_window();
let embedded_platform_window = platform_window
.as_headed_window()
.expect("No headed window");
if let Err(error) = embedded_platform_window.rendering_context.take_window() {
warn!("Unbinding native surface from context failed ({error:?})");
}
self.spin_event_loop();
}
@@ -606,8 +644,11 @@ impl App {
) {
let window_handle = unsafe { WindowHandle::borrow_raw(window_handle) };
let size = viewport_rect.size.to_u32();
if let Err(error) = self
.platform_window
let platform_window = self.window().platform_window();
let embedded_platform_window = platform_window
.as_headed_window()
.expect("No headed window");
if let Err(error) = embedded_platform_window
.rendering_context
.set_window(window_handle, PhysicalSize::new(size.width, size.height))
{

View File

@@ -2,7 +2,57 @@
* 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/. */
#![allow(non_snake_case)]
/// Servoshell for OpenHarmony
///
/// ## Overview
///
/// Apps on OpenHarmony are written in ArkTS and use ArkUI.
/// Servoshell is a native Rust application, which we compile as shared library,
/// which can communicate with ArkTS via NAPI.
/// The ArkTS portion of the code lives under `support/openharmony`, from the root of this
/// repository.
/// The ArkUI `XComponent` component, provides a surface that we can render to using OpenGL.
///
/// ## App Initialization
///
/// When the ArkTS app is loaded, the entrypoint is in `EntryAbility.ets` (since is the default and
/// also only ability of our ArkTS application). At the top of this file we have a series of `import`
/// commands, which import from various ArkTS libraries. One of those lines is
/// ```
/// import servoshell from 'libservoshell.so';
/// ```
///
/// This import statement will cause our shared library to be `dlopen`ed and the runtime will call
/// into our library. At this point autogenerated registration code for all functions annotated with
/// `#[napi]` will be run to register them with the N-API runtime.
/// The [napi-rs initialization documentation](https://napi.rs/docs/concepts/module-init) provides
/// more details on the initialization process at this stage.
///
/// Additionally our [`init()`] function that is annotated with `#[napi(module_exports)]` will run,
/// which is necessary (later) to register our callbacks for XComponent. At this point however,
/// there is no XComponent, and our init function will just initialize logging and return.
///
/// Execution then continues back in `EntryAbility.ets` with `EntryAbility::OnCreate()`, where
/// we parse the arguments our application received and then call [`init_servo()`], which spawns
/// the new main thread of the rust application and returns. Note that this is not the same as the
/// main thread of ArkUI.
///
/// The servoshell [`main_thread()`] continues to initialize servo, while concurrently the ArkTS
/// code continues with `onWindowStageCreate()`, which will load our ArkUI page from
/// `pages/Index.ets`. Once the page has been created, the `onForeground()` method will be invoked
/// by ArkTS, in which we currently do nothing.
/// Immediately afterwards, `aboutToAppear()` from our UIComponent in `Index.ets` will be invoked.
/// We override this method, and use it to register callbacks with servoshell, that will allow
/// servoshell to send events to ArkTS.
///
/// Next, during construction of the ArkUI, the `XComponent` surface will be created, which will
/// in turn cause [`init()`] to be invoked (again), this time with the xcomponent object.
/// `servoshell` registers callbacks for the xcomponent object.
/// After [`init()`] finishes, ArkTS will call the callback for `on_surface_created`, that we just
/// registered. In the callback we send a message to our main thread, informing it of the new window.
/// Additionally, for the first window, we also setup vsync callbacks.
///
/// At this point the initialization is finished, and servoshell is ready.
mod resources;
use std::cell::RefCell;
@@ -11,14 +61,14 @@ use std::os::raw::c_void;
use std::path::PathBuf;
use std::ptr::NonNull;
use std::rc::Rc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{Receiver, Sender};
use std::sync::{LazyLock, Mutex, Once, OnceLock, mpsc};
use std::thread::sleep;
use std::time::Duration;
use std::{fs, thread};
use dpi::PhysicalSize;
use euclid::{Point2D, Rect, Size2D};
use euclid::{Point2D, Rect, Scale, Size2D};
use keyboard_types::{Key, NamedKey};
use log::{LevelFilter, debug, error, info, trace, warn};
use napi_derive_ohos::napi;
@@ -50,7 +100,7 @@ use xcomponent_sys::{
OH_NativeXComponent_TouchEvent, OH_NativeXComponent_TouchEventType,
};
use super::app::{App, AppInitOptions, VsyncRefreshDriver};
use super::app::{App, AppInitOptions, EmbeddedPlatformWindow, VsyncRefreshDriver};
use super::host_trait::HostTrait;
use crate::egl::ohos::resources::ResourceReaderInstance;
use crate::prefs::{ArgumentParsingResult, parse_command_line_arguments};
@@ -103,6 +153,21 @@ struct NativeValues {
os_full_name: String,
}
fn get_display_density() -> f32 {
let mut density: f32 = 0_f32;
unsafe {
if let Err(error_code) =
display_manager::OH_NativeDisplayManager_GetDefaultDisplayDensityPixels(&mut density)
{
warn!(
"Could not get default display density due to display manager error: {error_code:?}"
);
return 1.0;
}
}
density
}
/// Gets the resource and cache directory from the native c methods.
fn get_native_values() -> NativeValues {
let cache_dir = {
@@ -120,29 +185,19 @@ fn get_native_values() -> NativeValues {
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,
display_density: get_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.
/// Initialize the servoshell [`App`].
fn init_app(
options: InitOpts,
native_window: *mut c_void,
xcomponent: *mut OH_NativeXComponent,
event_loop_waker: Box<dyn EventLoopWaker>,
host: Box<dyn HostTrait>,
) -> Result<Rc<App>, &'static str> {
info!("Entered servoshell init function");
crate::init_crypto();
@@ -222,29 +277,9 @@ fn init_app(
#[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,
host: Rc::new(HostCallbacks::new()),
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,
@@ -312,13 +347,13 @@ pub(super) enum ServoAction {
ImeDeleteForward(usize),
ImeDeleteBackward(usize),
ImeSendEnter,
Initialize(Box<InitOpts>),
Vsync,
Resize {
width: i32,
height: i32,
},
FocusWebview(u32),
CreatePlatformWindow(XComponentWrapper, WindowWrapper),
NewWebview(XComponentWrapper, WindowWrapper),
}
@@ -378,9 +413,6 @@ impl ServoAction {
servo.key_down(Key::Named(NamedKey::Enter));
servo.key_up(Key::Named(NamedKey::Enter));
},
Initialize(_init_opts) => {
panic!("Received Initialize event, even though servo is already initialized")
},
Vsync => {
servo.notify_vsync();
},
@@ -414,6 +446,32 @@ impl ServoAction {
error!("Could not find webview to activate");
}
},
CreatePlatformWindow(xcomponent, native_window) => {
let (window_handle, viewport_rect) =
get_raw_window_handle(xcomponent.0, native_window.0);
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 hidpi_factor = Scale::new(get_display_density());
servo.add_platform_window(
display_handle,
window_handle,
viewport_rect,
hidpi_factor,
);
// TODO: creating the window and creating the webview should be separate.
let webview = servo.create_and_activate_toplevel_webview(servo.initial_url());
let id = webview.id();
NATIVE_WEBVIEWS
.lock()
.unwrap()
.push(NativeWebViewComponents {
id,
xcomponent: xcomponent.clone(),
window: native_window.clone(),
});
},
NewWebview(xcomponent, window) => {
servo.pause_painting();
let webview =
@@ -465,47 +523,18 @@ unsafe extern "C" fn on_vsync_cb(
}
}
fn main_thread(xc: XComponentWrapper, window: WindowWrapper) {
fn main_thread(init_opts: InitOpts) {
let (tx, rx): (Sender<ServoAction>, Receiver<ServoAction>) = mpsc::channel();
SERVO_CHANNEL
.set(tx.clone())
.expect("Servo channel already initialized");
log::info!("Servo main-thread channel initialized");
let wakeup = Box::new(WakeupCallback::new(tx));
let callbacks = Box::new(HostCallbacks::new());
let init_opts = if let Ok(ServoAction::Initialize(init_opts)) = rx.recv() {
init_opts
} else {
panic!("Servos GL thread received another event before it was initialized")
};
let servo = init_app(*init_opts, window.0, xc.0, wakeup, callbacks)
.expect("Servo initialization failed");
let id = servo
.active_or_newest_webview()
.expect("Should always start with at least one WebView")
.id();
NATIVE_WEBVIEWS
.lock()
.unwrap()
.push(NativeWebViewComponents {
id,
xcomponent: xc,
window,
});
info!("Surface created!");
let native_vsync =
ohos_vsync::NativeVsync::new("ServoVsync").expect("Failed to create NativeVsync");
// get_period() returns an error - perhaps we need to wait until the first callback?
// info!("Native vsync period is {} nanoseconds", native_vsync.get_period().unwrap());
unsafe {
native_vsync
.request_raw_callback_with_self(Some(on_vsync_cb))
.expect("Failed to request vsync callback")
}
info!("Enabled Vsync!");
let servo = init_app(init_opts, wakeup).expect("Servo initialization failed");
while let Ok(action) = rx.recv() {
trace!("Wakeup message received!");
@@ -517,6 +546,7 @@ fn main_thread(xc: XComponentWrapper, window: WindowWrapper) {
#[unsafe(no_mangle)]
extern "C" fn on_surface_created_cb(xcomponent: *mut OH_NativeXComponent, window: *mut c_void) {
static FIRST_WINDOW: AtomicBool = AtomicBool::new(true);
info!("on_surface_created_cb");
#[cfg(feature = "tracing-hitrace")]
let _ = hitrace::ScopedTrace::start_trace(&c"on_surface_created_cb");
@@ -524,17 +554,29 @@ extern "C" fn on_surface_created_cb(xcomponent: *mut OH_NativeXComponent, window
let xc_wrapper = XComponentWrapper(xcomponent);
let window_wrapper = WindowWrapper(window);
if SERVO_CHANNEL.get().is_none() {
// Todo: Perhaps it would be better to move this thread into the vsync signal thread.
// This would allow us to save one thread and the IPC for the vsync signal.
//
// Each thread will send its id via the channel
let _main_surface_thread = thread::spawn(move || {
main_thread(xc_wrapper, window_wrapper);
});
// TODO: This if will be removed once we add multi-window support in a follow-up PR.
// This function will always be invoked on the UI thread, so there is no concurrency.
if FIRST_WINDOW.load(Ordering::Relaxed) {
FIRST_WINDOW.store(false, Ordering::Relaxed);
// The servo event loop is initialized before the native window / xcomponent is created,
// so if this fails, servo has crashed and we don't have a way to recover.
call(ServoAction::CreatePlatformWindow(
xc_wrapper,
window_wrapper,
))
.expect("Servo main thread channel not initialized");
let native_vsync =
ohos_vsync::NativeVsync::new("ServoVsync").expect("Failed to create NativeVsync");
unsafe {
native_vsync
.request_raw_callback_with_self(Some(on_vsync_cb))
.expect("Failed to request vsync callback")
}
info!("Enabled Vsync!");
} else {
call(ServoAction::NewWebview(xc_wrapper, window_wrapper))
.expect("Could not create new webview");
.expect("Servo main thread channel not initialized");
}
info!("Returning from on_surface_created_cb");
}
@@ -832,6 +874,7 @@ fn init(exports: Object, env: Env) -> napi_ohos::Result<()> {
info!("servoshell init function called");
if let Ok(xcomponent) = exports.get_named_property::<Object>("__NATIVE_XCOMPONENT_OBJ__") {
register_xcomponent_callbacks(&env, &xcomponent)?;
info!("Registered Xcomponent callbacks!");
}
info!("Finished init");
@@ -891,29 +934,10 @@ pub fn register_prompt_toast_callback(callback: Function<String, ()>) -> napi_oh
#[napi]
pub fn init_servo(init_opts: InitOpts) -> napi_ohos::Result<()> {
info!("Servo is being initialised with the following Options: ");
info!("Initial URL: {}", init_opts.url);
let channel = if let Some(channel) = SERVO_CHANNEL.get() {
channel
} else {
warn!("Servo GL thread has not initialized yet. Retrying.....");
let mut iter_count = 0;
loop {
if let Some(channel) = SERVO_CHANNEL.get() {
break channel;
} else {
iter_count += 1;
if iter_count > 10 {
error!("Servo GL thread not reachable");
panic!("Servo GL thread not reachable");
}
sleep(Duration::from_millis(100));
}
}
};
channel
.send(ServoAction::Initialize(Box::new(init_opts)))
.expect("Failed to connect to servo GL thread");
info!("Servo is being initialised with the following Options: {init_opts:?}");
let _main_surface_thread = thread::spawn(move || {
main_thread(init_opts);
});
Ok(())
}

View File

@@ -1 +1,14 @@
export const loadURL: (url: string) => void;
export interface InitOpts {
url: string;
resourceDir: string,
commandlineArgs: string,
}
export const loadURL: (url: string) => void;
export const goBack: () => void;
export const goForward: () => void;
export const registerURLcallback: (callback: (url: string) => void) => void;
export const registerTerminateCallback: (callback: () => void) => void;
export const registerPromptToastCallback: (callback: (msg: string) => void) => void;
export const focusWebview:(index: number) => void;
export const initServo:(options: InitOpts) => void;

View File

@@ -3,6 +3,7 @@ import hilog from '@ohos.hilog';
import UIAbility from '@ohos.app.ability.UIAbility';
import Want from '@ohos.app.ability.Want';
import window from '@ohos.window';
import servoshell, { InitOpts } from 'libservoshell.so';
const WRONG_COMMAND_ARRAY = ["tracing", "devtools", "force_ipc", "multiprocess", "webdriver"];
@@ -29,11 +30,11 @@ export default class EntryAbility extends UIAbility {
// See the aa tool documentation for more details:
// https://docs.openharmony.cn/pages/v5.0/en/application-dev/tools/aa-tool.md
if (key.startsWith("--")) {
let value = entry[1].toString()
params.push(key)
if (value) {
params.push(value)
}
let value = entry[1].toString()
params.push(key)
if (value) {
params.push(value)
}
} else if (WRONG_COMMAND_ARRAY.some(value => key.startsWith(value))) {
hilog.error(0xE0C3, "Servo EntryAbility", "-------------------------------------------------------------------------------------------------");
hilog.error(0xE0C3, "Servo EntryAbility", "\n\nYou probably meant to add -- infront of your argument, i.e., --psn=--tracing vs --psn=tracing. You used " + entry);
@@ -44,8 +45,15 @@ export default class EntryAbility extends UIAbility {
}
let servoshell_params = params.join("\u{001f}")
hilog.debug(0x0000, 'Servo EntryAbility', 'Servoshell parameters: %{public}s', servoshell_params);
this.init_params.setOrCreate('InitialURI', uri)
this.init_params.setOrCreate('CommandlineArgs', servoshell_params)
let resource_dir: string = this.context.resourceDir;
console.debug("resourceDir: ", resource_dir);
let init_options: InitOpts = {
url: uri,
resourceDir: resource_dir,
commandlineArgs: servoshell_params,
}
servoshell.initServo(init_options)
}
onDestroy() {

View File

@@ -1,30 +1,5 @@
import { common } from '@kit.AbilityKit';
import display from '@ohos.display';
import deviceInfo from '@ohos.deviceInfo';
import promptAction from '@ohos.promptAction';
interface ServoXComponentInterface {
loadURL(url: string): void;
goBack(): void;
goForward(): void;
registerURLcallback(callback: (url: string) => void): void;
registerTerminateCallback(callback: () => void): void;
registerPromptToastCallback(callback: (msg: string) => void): void
focusWebview(index: number):void;
initServo(options: InitOpts): void;
}
interface InitOpts {
url: string;
resourceDir: string,
commandlineArgs: string,
}
import servoshell from 'libservoshell.so';
function prompt_toast(msg: string) {
promptAction.showToast({
@@ -39,23 +14,29 @@ let storage = LocalStorage.getShared()
@Entry(storage)
@Component
struct Index {
xComponentContext: ServoXComponentInterface | undefined = undefined;
xComponentAttrs: XComponentAttrs = {
id: 'ServoDemo',
type: XComponentType.SURFACE,
libraryname: 'servoshell',
}
private context = getContext(this) as common.UIAbilityContext;
@LocalStorageProp('InitialURI') InitialURI: string = "unused"
@LocalStorageProp('CommandlineArgs') CommandlineArgs: string = ""
@State urlToLoad: string = this.InitialURI
@State tablist: Array<number> = [];
@State urlToLoad: string = ""
@State tablist: Array<number> = [1];
@State currentIndex: number = 0;
aboutToAppear(): void {
console.info("aboutToAppear - registering callbacks!")
servoshell.registerURLcallback((new_url: string) => {
console.info('New URL from native: ', new_url)
this.urlToLoad = new_url
})
servoshell.registerPromptToastCallback(prompt_toast)
}
// Called when the user swipes from the right or left edge to the middle
// Default behavior is bringing the app to the background.
onBackPress(): boolean | void {
this.xComponentContext?.goBack()
onBackPress(): boolean {
servoshell.goBack();
return true;
}
@@ -72,15 +53,8 @@ struct Index {
.fontSize(22)
.width('12%')
.onClick((event) => {
if (this.tablist.length==0) {
this.tablist.push(2);
} else {
this.tablist.push(this.tablist[this.tablist.length-1]+1);
}
// yes this is correct as we always have one tab extra
// The tab extra is seperate for the initialization and will always exist.
// It is not in the tablist.
this.currentIndex = this.tablist.length;
this.tablist.push(this.tablist[this.tablist.length-1]+1);
this.currentIndex = this.tablist.length - 1;
})
Button('⇦')
.backgroundColor(Color.White)
@@ -98,7 +72,7 @@ struct Index {
.fontSize(12)
.width('12%')
.onClick(() => {
this.xComponentContext?.goForward()
servoshell.goForward()
})
TextInput({ placeholder: 'URL', text: $$this.urlToLoad })
.type(InputType.URL)
@@ -107,34 +81,12 @@ struct Index {
this.urlToLoad = value
})
.onSubmit((EnterKeyType) => {
this.xComponentContext?.loadURL(this.urlToLoad)
servoshell.loadURL(this.urlToLoad)
console.info('Load URL: ', this.urlToLoad)
})
}
Tabs({ barPosition: BarPosition.Start, index: this.currentIndex}) {
TabContent() {
XComponent(this.xComponentAttrs)
.focusable(true)
.onLoad((xComponentContext) => {
this.xComponentContext = xComponentContext as ServoXComponentInterface;
let resource_dir: string = this.context.resourceDir;
let cache_dir: string = this.context.cacheDir;
console.debug("resourceDir: ", resource_dir);
console.debug("cacheDir: ", cache_dir);
let init_options: InitOpts = {
url: this.urlToLoad,
resourceDir: resource_dir,
commandlineArgs: this.CommandlineArgs
}
this.xComponentContext.initServo(init_options)
this.xComponentContext.registerURLcallback((new_url) => {
console.info('New URL from native: ', new_url)
this.urlToLoad = new_url
})
this.xComponentContext.registerPromptToastCallback(prompt_toast)
})
}.tabBar('1')
ForEach(this.tablist, (item: number) => {
TabContent() {
XComponent(this.xComponentAttrs)
@@ -142,7 +94,8 @@ struct Index {
}.tabBar(String(item))
})
}.onChange((index: number) => {
this.xComponentContext?.focusWebview(index);
console.info("Focusing Tab number %d", index)
servoshell.focusWebview(index);
})
}
.width('100%')