diff --git a/components/compositing/touch.rs b/components/compositing/touch.rs index 76d87732b32..1fe8d14817d 100644 --- a/components/compositing/touch.rs +++ b/components/compositing/touch.rs @@ -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, ) -> 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) { - 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() diff --git a/components/compositing/webview_renderer.rs b/components/compositing/webview_renderer.rs index b0e91ccb02e..84eddac0b76 100644 --- a/components/compositing/webview_renderer.rs +++ b/components/compositing/webview_renderer.rs @@ -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 diff --git a/components/shared/embedder/input_events.rs b/components/shared/embedder/input_events.rs index ff48bd7b7d8..af7ee3d7ceb 100644 --- a/components/shared/embedder/input_events.rs +++ b/components/shared/embedder/input_events.rs @@ -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); diff --git a/ports/servoshell/egl/app_state.rs b/ports/servoshell/egl/app_state.rs index 114c9c5f4d0..735b26959f6 100644 --- a/ports/servoshell/egl/app_state.rs +++ b/ports/servoshell/egl/app_state.rs @@ -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 { self.inner.borrow() } @@ -371,14 +380,14 @@ impl RunningAppState { Ok(webview_id) } - fn newest_webview(&self) -> Option { + pub(crate) fn newest_webview(&self) -> Option { 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()) diff --git a/ports/servoshell/egl/ohos.rs b/ports/servoshell/egl/ohos.rs index b9587771a50..daad6cfa2fe 100644 --- a/ports/servoshell/egl/ohos.rs +++ b/ports/servoshell/egl/ohos.rs @@ -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, > = 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> = 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) { 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, Receiver) = 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, Receiver) = 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, diff --git a/ports/servoshell/egl/ohos/simpleservo.rs b/ports/servoshell/egl/ohos/simpleservo.rs index c867c7a5330..25ff8255442 100644 --- a/ports/servoshell/egl/ohos/simpleservo.rs +++ b/ports/servoshell/egl/ohos/simpleservo.rs @@ -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, 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( diff --git a/support/openharmony/entry/src/main/ets/pages/Index.ets b/support/openharmony/entry/src/main/ets/pages/Index.ets index 9c6cb06537b..a9dfbb29618 100644 --- a/support/openharmony/entry/src/main/ets/pages/Index.ets +++ b/support/openharmony/entry/src/main/ets/pages/Index.ets @@ -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 = []; + @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; -} \ No newline at end of file +}