This commit is contained in:
Narfinger 2025-06-03 14:53:49 +02:00 committed by GitHub
commit 70162c7406
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 258 additions and 99 deletions

View file

@ -292,6 +292,10 @@ impl TouchHandler {
.expect("Current Touch sequence does not exist")
}
fn try_get_current_touch_sequence_mut(&mut self) -> Option<&mut TouchSequenceInfo> {
self.touch_sequence_map.get_mut(&self.current_sequence_id)
}
pub(crate) fn get_touch_sequence(&self, sequence_id: TouchSequenceId) -> &TouchSequenceInfo {
self.touch_sequence_map
.get(&sequence_id)
@ -374,7 +378,12 @@ impl TouchHandler {
id: TouchId,
point: Point2D<f32, DevicePixel>,
) -> TouchMoveAction {
let touch_sequence = self.get_current_touch_sequence_mut();
// As `TouchHandler` is per `WebViewRenderer` which is per `WebView` we might get a Touch Sequence Move that
// started with a down on a different webview. As the touch_sequence id is only changed on touch_down this
// move event gets a touch id which is already cleaned up.
let Some(touch_sequence) = self.try_get_current_touch_sequence_mut() else {
return TouchMoveAction::NoAction;
};
let idx = match touch_sequence
.active_touch_points
.iter_mut()
@ -529,7 +538,10 @@ impl TouchHandler {
}
pub fn on_touch_cancel(&mut self, id: TouchId, _point: Point2D<f32, DevicePixel>) {
let touch_sequence = self.get_current_touch_sequence_mut();
// A similar thing with touch move can happen here where the event is coming from a different webview.
let Some(touch_sequence) = self.try_get_current_touch_sequence_mut() else {
return;
};
match touch_sequence
.active_touch_points
.iter()

View file

@ -404,6 +404,8 @@ impl WebViewRenderer {
self.dispatch_input_event(event);
}
/// Send a [`TouchEvent`] to the Constellation for this [`WebViewRenderer`].
/// Returns true if the event was send
fn send_touch_event(&self, mut event: TouchEvent) -> bool {
let get_pipeline_details = |pipeline_id| self.pipelines.get(&pipeline_id);
let Some(result) = self

View file

@ -180,6 +180,7 @@ pub enum TouchEventType {
pub struct TouchId(pub i32);
/// An ID for a sequence of touch events between a `Down` and the `Up` or `Cancel` event.
/// The ID is the same for all events between `Down` and `Up` or `Cancel`
#[repr(transparent)]
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct TouchSequenceId(u32);

View file

@ -355,6 +355,15 @@ impl RunningAppState {
self.inner_mut().webviews.insert(webview.id(), 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.inner().webviews.get(&id) {
webview.focus();
} else {
error!("We could not find the webview with this id {id}");
}
}
fn inner(&self) -> Ref<RunningAppStateInner> {
self.inner.borrow()
}
@ -371,14 +380,14 @@ impl RunningAppState {
Ok(webview_id)
}
fn newest_webview(&self) -> Option<WebView> {
pub(crate) fn newest_webview(&self) -> Option<WebView> {
self.inner()
.creation_order
.last()
.and_then(|id| self.inner().webviews.get(id).cloned())
}
fn active_webview(&self) -> WebView {
pub(crate) fn active_webview(&self) -> WebView {
self.inner()
.focused_webview_id
.and_then(|id| self.inner().webviews.get(&id).cloned())

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;
@ -23,7 +24,7 @@ use ohos_ime_sys::types::InputMethod_EnterKeyType;
use servo::style::Zero;
use servo::{
AlertResponse, EventLoopWaker, InputMethodType, LoadStatus, MediaSessionPlaybackState,
PermissionRequest, SimpleDialog, WebView,
PermissionRequest, SimpleDialog, WebView, WebViewId,
};
use xcomponent_sys::{
OH_NativeXComponent, OH_NativeXComponent_Callback, OH_NativeXComponent_GetKeyEvent,
@ -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,20 @@ static PROMPT_TOAST: OnceLock<
ThreadsafeFunction<String, (), String, false, false, PROMPT_QUEUE_SIZE>,
> = OnceLock::new();
/// Storing webview related items
struct NativeWebViewComponents {
/// The id of the related webview
id: WebViewId,
/// The XComponentWrapper for the above webview
xcomponent: XComponentWrapper,
/// The WindowWrapper for the above webview
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<Vec<NativeWebViewComponents>> = Mutex::new(Vec::new());
impl ServoAction {
fn dispatch_touch_event(
servo: &RunningAppState,
@ -145,7 +163,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 +175,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 +203,56 @@ impl ServoAction {
servo.present_if_needed();
},
Resize { width, height } => servo.resize(Coordinates::new(0, 0, *width, *height)),
FocusWebview(arkts_id) => {
if let Some(native_webview_components) =
NATIVE_WEBVIEWS.lock().unwrap().get(*arkts_id as usize)
{
if (servo.active_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(
native_webview_components.xcomponent.0,
native_webview_components.window.0,
);
servo.resume_compositor(window_handle, coordinates);
let url = servo
.active_webview()
.url()
.map(|u| u.to_string())
.unwrap_or(String::from("about:blank"));
SET_URL_BAR_CB
.get()
.map(|f| f.call(url, ThreadsafeFunctionCallMode::Blocking));
}
} else {
error!("Could not find webview to focus");
}
},
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);
servo.resume_compositor(window_handle, coordinates);
let webview = servo.newest_webview().expect("There should always be one");
let id = webview.id();
NATIVE_WEBVIEWS
.lock()
.unwrap()
.push(NativeWebViewComponents {
id: id,
xcomponent: xcomponent.clone(),
window: window.clone(),
});
let url = webview
.url()
.map(|u| u.to_string())
.unwrap_or(String::from("about:blank"));
SET_URL_BAR_CB
.get()
.map(|f| f.call(url, ThreadsafeFunctionCallMode::Blocking));
},
};
}
}
@ -223,50 +291,64 @@ 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!");
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");
while let Ok(action) = rx.recv() {
trace!("Wakeup message received!");
action.do_action(&servo);
}
NATIVE_WEBVIEWS
.lock()
.unwrap()
.push(NativeWebViewComponents {
id: servo.active_webview().id(),
xcomponent: xc,
window,
});
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 +726,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));
(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.
@ -81,57 +89,89 @@ struct Index {
// Flex.
Flex({ direction: FlexDirection.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%')
}
@ -141,4 +181,4 @@ interface XComponentAttrs {
id: string;
type: number;
libraryname: string;
}
}