Allow OHOS servoshell to have a simple multiple tab implementation.

Currently we just pause the compositor and replace the window in it
while having separate bookkeeping to remember which window belongs to
which tab.

Signed-off-by: Narfinger <Narfinger@users.noreply.github.com>
This commit is contained in:
Narfinger 2025-05-07 11:01:23 +02:00
parent e2424fcec7
commit 5bb60f47fc
4 changed files with 244 additions and 122 deletions

View file

@ -96,6 +96,9 @@ struct RunningAppStateInner {
/// The HiDPI scaling factor to use for the display of [`WebView`]s.
hidpi_scale_factor: Scale<f32, DeviceIndependentPixel, DevicePixel>,
/// Touch events should not be processed if the compositor is paused
compositor_paused: bool,
}
struct ServoShellServoDelegate {
@ -314,6 +317,7 @@ impl RunningAppState {
focused_webview_id: None,
animating_state_changed,
hidpi_scale_factor: Scale::new(hidpi_scale_factor),
compositor_paused: false,
}),
});
@ -337,6 +341,19 @@ impl RunningAppState {
self.inner_mut().webviews.insert(webview.id(), webview);
}
pub(crate) fn activate_webview(&self, id: u32) {
let inner = self.inner();
let webview = inner
.creation_order
.get(id as usize)
.and_then(|id| inner.webviews.get(id));
if let Some(webview) = webview {
webview.focus();
} else {
error!("We could not find the webview with this id {id}");
}
}
fn inner(&self) -> Ref<RunningAppStateInner> {
self.inner.borrow()
}
@ -492,46 +509,54 @@ impl RunningAppState {
/// Touch event: press down
pub fn touch_down(&self, x: f32, y: f32, pointer_id: i32) {
self.active_webview()
.notify_input_event(InputEvent::Touch(TouchEvent::new(
TouchEventType::Down,
TouchId(pointer_id),
Point2D::new(x, y),
)));
self.perform_updates();
if !self.inner().compositor_paused {
self.active_webview()
.notify_input_event(InputEvent::Touch(TouchEvent::new(
TouchEventType::Down,
TouchId(pointer_id),
Point2D::new(x, y),
)));
self.perform_updates();
}
}
/// Touch event: move touching finger
pub fn touch_move(&self, x: f32, y: f32, pointer_id: i32) {
self.active_webview()
.notify_input_event(InputEvent::Touch(TouchEvent::new(
TouchEventType::Move,
TouchId(pointer_id),
Point2D::new(x, y),
)));
self.perform_updates();
if !self.inner().compositor_paused {
self.active_webview()
.notify_input_event(InputEvent::Touch(TouchEvent::new(
TouchEventType::Move,
TouchId(pointer_id),
Point2D::new(x, y),
)));
self.perform_updates();
}
}
/// Touch event: Lift touching finger
pub fn touch_up(&self, x: f32, y: f32, pointer_id: i32) {
self.active_webview()
.notify_input_event(InputEvent::Touch(TouchEvent::new(
TouchEventType::Up,
TouchId(pointer_id),
Point2D::new(x, y),
)));
self.perform_updates();
if !self.inner().compositor_paused {
self.active_webview()
.notify_input_event(InputEvent::Touch(TouchEvent::new(
TouchEventType::Up,
TouchId(pointer_id),
Point2D::new(x, y),
)));
self.perform_updates();
}
}
/// Cancel touch event
pub fn touch_cancel(&self, x: f32, y: f32, pointer_id: i32) {
self.active_webview()
.notify_input_event(InputEvent::Touch(TouchEvent::new(
TouchEventType::Cancel,
TouchId(pointer_id),
Point2D::new(x, y),
)));
self.perform_updates();
if !self.inner().compositor_paused {
self.active_webview()
.notify_input_event(InputEvent::Touch(TouchEvent::new(
TouchEventType::Cancel,
TouchId(pointer_id),
Point2D::new(x, y),
)));
self.perform_updates();
}
}
/// Register a mouse movement.
@ -637,6 +662,7 @@ impl RunningAppState {
if let Err(e) = self.rendering_context.take_window() {
warn!("Unbinding native surface from context failed ({:?})", e);
}
self.inner_mut().compositor_paused = true;
self.perform_updates();
}
@ -649,6 +675,7 @@ impl RunningAppState {
{
warn!("Binding native surface to context failed ({:?})", e);
}
self.inner_mut().compositor_paused = false;
self.perform_updates();
}

View file

@ -6,8 +6,9 @@
use std::cell::RefCell;
use std::mem::MaybeUninit;
use std::os::raw::c_void;
use std::rc::Rc;
use std::sync::mpsc::{Receiver, Sender};
use std::sync::{LazyLock, Once, OnceLock, mpsc};
use std::sync::{LazyLock, Mutex, Once, OnceLock, mpsc};
use std::thread;
use std::thread::sleep;
use std::time::Duration;
@ -69,9 +70,11 @@ fn call(action: ServoAction) -> Result<(), CallError> {
}
#[repr(transparent)]
struct XComponentWrapper(*mut OH_NativeXComponent);
#[derive(Clone)]
pub(crate) struct XComponentWrapper(*mut OH_NativeXComponent);
#[repr(transparent)]
struct WindowWrapper(*mut c_void);
#[derive(Clone)]
pub(crate) struct WindowWrapper(*mut c_void);
unsafe impl Send for XComponentWrapper {}
unsafe impl Send for WindowWrapper {}
@ -84,7 +87,6 @@ pub(super) enum TouchEventType {
Unknown,
}
#[derive(Debug)]
pub(super) enum ServoAction {
WakeUp,
LoadUrl(String),
@ -108,6 +110,8 @@ pub(super) enum ServoAction {
width: i32,
height: i32,
},
FocusWebview(u32),
NewWebview(XComponentWrapper, WindowWrapper),
}
/// Queue length for the thread-safe function to submit URL updates to ArkTS
@ -127,6 +131,11 @@ static PROMPT_TOAST: OnceLock<
ThreadsafeFunction<String, (), String, false, false, PROMPT_QUEUE_SIZE>,
> = 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 WEBVIEW_TO_RAW_HANDLE: Mutex<Vec<(XComponentWrapper, WindowWrapper)>> =
Mutex::new(Vec::new());
impl ServoAction {
fn dispatch_touch_event(
servo: &RunningAppState,
@ -145,7 +154,7 @@ impl ServoAction {
}
// todo: consider making this take `self`, so we don't need to needlessly clone.
fn do_action(&self, servo: &RunningAppState) {
fn do_action(&self, servo: &Rc<RunningAppState>) {
use ServoAction::*;
match self {
WakeUp => servo.perform_updates(),
@ -157,7 +166,7 @@ impl ServoAction {
x,
y,
pointer_id,
} => Self::dispatch_touch_event(servo, *kind, *x, *y, *pointer_id),
} => Self::dispatch_touch_event(&servo, *kind, *x, *y, *pointer_id),
KeyUp(k) => servo.key_up(k.clone()),
KeyDown(k) => servo.key_down(k.clone()),
InsertText(text) => servo.ime_insert_text(text.clone()),
@ -185,6 +194,29 @@ impl ServoAction {
servo.present_if_needed();
},
Resize { width, height } => servo.resize(Coordinates::new(0, 0, *width, *height)),
FocusWebview(id) => {
servo.activate_webview(id.clone());
servo.pause_compositor();
let webview_lock = WEBVIEW_TO_RAW_HANDLE.lock().unwrap();
let (xcomponent_wrapper, window_wrapper) = webview_lock
.get(*id as usize)
.clone()
.expect("Could not find window handle to webview");
let (window_handle, _, coordinates) =
simpleservo::get_raw_window_handle(xcomponent_wrapper.0, window_wrapper.0);
servo.resume_compositor(window_handle, coordinates);
},
NewWebview(xcomponent, window) => {
servo.pause_compositor();
servo.new_toplevel_webview("about:blank".parse().unwrap());
let (window_handle, _, coordinates) =
simpleservo::get_raw_window_handle(xcomponent.0, window.0);
WEBVIEW_TO_RAW_HANDLE
.lock()
.unwrap()
.push((xcomponent.clone(), window.clone()));
servo.resume_compositor(window_handle, coordinates);
},
};
}
}
@ -223,50 +255,60 @@ extern "C" fn on_surface_created_cb(xcomponent: *mut OH_NativeXComponent, window
let xc_wrapper = XComponentWrapper(xcomponent);
let window_wrapper = WindowWrapper(window);
// 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 || {
let (tx, rx): (Sender<ServoAction>, Receiver<ServoAction>) = mpsc::channel();
if !SERVO_CHANNEL.get().is_some() {
// 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 || {
let (tx, rx): (Sender<ServoAction>, Receiver<ServoAction>) = mpsc::channel();
SERVO_CHANNEL
.set(tx.clone())
.expect("Servo channel already initialized");
SERVO_CHANNEL
.set(tx.clone())
.expect("Servo channel already initialized");
let wakeup = Box::new(WakeupCallback::new(tx));
let callbacks = Box::new(HostCallbacks::new());
let wakeup = Box::new(WakeupCallback::new(tx));
let callbacks = Box::new(HostCallbacks::new());
let xc = xc_wrapper;
let window = window_wrapper;
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 = simpleservo::init(*init_opts, window.0, xc.0, wakeup, callbacks)
.expect("Servo initialization failed");
let xc = xc_wrapper;
let window = window_wrapper;
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!");
WEBVIEW_TO_RAW_HANDLE
.lock()
.unwrap()
.push((xc.clone(), window.clone()));
while let Ok(action) = rx.recv() {
trace!("Wakeup message received!");
action.do_action(&servo);
}
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 = simpleservo::init(*init_opts, window.0, xc.0, wakeup, callbacks)
.expect("Servo initialization failed");
info!("Sender disconnected - Terminating main surface thread");
});
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!");
while let Ok(action) = rx.recv() {
trace!("Wakeup message received!");
action.do_action(&servo);
}
info!("Sender disconnected - Terminating main surface thread");
});
} else {
call(ServoAction::NewWebview(xc_wrapper, window_wrapper))
.expect("Could not create new webview");
}
info!("Returning from on_surface_created_cb");
}
@ -644,6 +686,12 @@ pub fn init_servo(init_opts: InitOpts) -> napi_ohos::Result<()> {
Ok(())
}
#[napi]
fn focus_webview(id: u32) {
debug!("Focusing webview {id} from napi");
call(ServoAction::FocusWebview(id)).expect("Could not focus webview");
}
struct OhosImeOptions {
input_type: ohos_ime_sys::types::InputMethod_TextInputType,
enterkey_type: InputMethod_EnterKeyType,

View file

@ -23,6 +23,20 @@ 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<i32>, 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));
let coordinates = Coordinates::new(0, 0, window_size.width, window_size.height);
(window_handle, window_size, coordinates)
}
/// Initialize Servo. At that point, we need a valid GL context.
/// In the future, this will be done in multiple steps.
pub fn init(
@ -94,19 +108,12 @@ pub fn init(
#[cfg(target_env = "ohos")]
crate::egl::ohos::set_log_filter(servoshell_preferences.log_filter.as_deref());
let Ok(window_size) = (unsafe { super::get_xcomponent_size(xcomponent, native_window) }) else {
return Err("Failed to get xcomponent size");
};
let Ok((x, y)) = (unsafe { super::get_xcomponent_offset(xcomponent, native_window) }) else {
return Err("Failed to get xcomponent offset");
};
let coordinates = Coordinates::new(x, y, window_size.width, window_size.height);
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 native_window = NonNull::new(native_window).expect("Could not get native window");
let window_handle = RawWindowHandle::OhosNdk(OhosNdkWindowHandle::new(native_window));
let window_handle = unsafe { WindowHandle::borrow_raw(window_handle) };
let rendering_context = Rc::new(

View file

@ -5,10 +5,17 @@ import promptAction from '@ohos.promptAction';
interface ServoXComponentInterface {
loadURL(url: string): void;
goBack(): void;
goForward(): void;
registerURLcallback(callback: (url: string) => void): void;
registerPromptToastCallback(callback: (msg: string) => void): void
focusWebview(index: number):void;
initServo(options: InitOpts): void;
}
@ -23,20 +30,20 @@ interface InitOpts {
}
function get_density(): number {
try {
let displayClass = display.getDefaultDisplaySync();
console.info('Test densityDPI:' + JSON.stringify(displayClass.densityDPI));
return displayClass.densityDPI / 160;
try {
let displayClass = display.getDefaultDisplaySync();
console.info('Test densityDPI:' + JSON.stringify(displayClass.densityDPI));
return displayClass.densityDPI / 160;
} catch (exception) {
console.error('Failed to obtain the default display object. Code: ' + JSON.stringify(exception));
return 3;
console.error('Failed to obtain the default display object. Code: ' + JSON.stringify(exception));
return 3;
}
}
function get_device_type(): string {
let device_type: string = deviceInfo.deviceType;
if (device_type == "") {
console.error("deviceInfo.deviceType is empty string!")
console.error("deviceInfo.deviceType is empty string!")
} else {
console.info("Device type is " + device_type)
}
@ -44,10 +51,10 @@ function get_device_type(): string {
}
function prompt_toast(msg: string) {
promptAction.showToast({
message: msg,
duration: 2000
});
promptAction.showToast({
message: msg,
duration: 2000
});
}
// Use the getShared API to obtain the LocalStorage instance shared by stage.
@ -62,11 +69,12 @@ struct Index {
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 currentIndex: number = 0;
// Called when the user swipes from the right or left edge to the middle
// Default behavior is bringing the app to the background.
@ -78,57 +86,89 @@ struct Index {
build() {
Column() {
Row() {
Button('⇦').backgroundColor(Color.White)
Button('+')
.backgroundColor(Color.White)
.fontColor(Color.Black)
.fontWeight(FontWeight.Bolder)
.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;
})
Button('⇦')
.backgroundColor(Color.White)
.fontColor(Color.Black)
.fontWeight(FontWeight.Bolder)
.width('12%')
.fontSize(12)
.onClick(()=>{
.onClick(() => {
this.onBackPress()
})
Button('⇨').backgroundColor(Color.White)
Button('⇨')
.backgroundColor(Color.White)
.fontColor(Color.Black)
.fontWeight(FontWeight.Bolder)
.fontSize(12)
.width('12%')
.onClick(()=> {
.onClick(() => {
this.xComponentContext?.goForward()
})
TextInput({placeholder:'URL',text: $$this.urlToLoad})
TextInput({ placeholder: 'URL', text: $$this.urlToLoad })
.type(InputType.Normal)
.width('76%')
.onChange((value) => {
this.urlToLoad = value
})
.onSubmit((EnterKeyType)=>{
.onSubmit((EnterKeyType) => {
this.xComponentContext?.loadURL(this.urlToLoad)
console.info('Load URL: ', this.urlToLoad)
})
}
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,
deviceType: get_device_type(),
osFullName: deviceInfo.osFullName,
displayDensity: get_density(),
resourceDir: resource_dir,
cacheDir: cache_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)
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,
deviceType: get_device_type(),
osFullName: deviceInfo.osFullName,
displayDensity: get_density(),
resourceDir: resource_dir,
cacheDir: cache_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)
.focusable(true)
}.tabBar(String(item))
})
}.onChange((index: number) => {
this.xComponentContext?.focusWebview(index);
})
}
.width('100%')
}
@ -138,4 +178,4 @@ interface XComponentAttrs {
id: string;
type: number;
libraryname: string;
}
}