diff options
Diffstat (limited to 'ports')
-rw-r--r-- | ports/servoshell/Cargo.toml | 11 | ||||
-rw-r--r-- | ports/servoshell/backtrace.rs | 11 | ||||
-rw-r--r-- | ports/servoshell/build.rs | 20 | ||||
-rw-r--r-- | ports/servoshell/egl/android.rs | 6 | ||||
-rw-r--r-- | ports/servoshell/egl/android/simpleservo.rs | 814 | ||||
-rw-r--r-- | ports/servoshell/egl/gl_glue.rs | 1 | ||||
-rw-r--r-- | ports/servoshell/egl/host_trait.rs | 75 | ||||
-rw-r--r-- | ports/servoshell/egl/mod.rs | 9 | ||||
-rw-r--r-- | ports/servoshell/egl/ohos.rs | 619 | ||||
-rw-r--r-- | ports/servoshell/egl/ohos/simpleservo.rs | 110 | ||||
-rw-r--r-- | ports/servoshell/egl/resources.rs | 56 | ||||
-rw-r--r-- | ports/servoshell/egl/servo_glue.rs | 701 | ||||
-rw-r--r-- | ports/servoshell/lib.rs | 5 | ||||
-rw-r--r-- | ports/servoshell/main.rs | 9 |
14 files changed, 1643 insertions, 804 deletions
diff --git a/ports/servoshell/Cargo.toml b/ports/servoshell/Cargo.toml index 33c5fa32d5c..8ba4692907a 100644 --- a/ports/servoshell/Cargo.toml +++ b/ports/servoshell/Cargo.toml @@ -60,6 +60,8 @@ log = { workspace = true } lazy_static = { workspace = true } getopts = { workspace = true } url = { workspace = true } +servo-media = { workspace = true } + [target.'cfg(target_os = "android")'.dependencies] android_logger = "0.14" @@ -67,7 +69,6 @@ ipc-channel = { workspace = true } jni = "0.21.1" libloading = "0.8" serde_json = { workspace = true } -servo-media = { workspace = true } surfman = { workspace = true, features = ["sm-angle-default"] } webxr = { git = "https://github.com/servo/webxr" } @@ -79,6 +80,13 @@ backtrace = { workspace = true } # force inprocess, until libc-rs 0.2.156 is released containing # https://github.com/rust-lang/libc/commit/9e248e212c5602cb4e98676e4c21ea0382663a12 ipc-channel = { workspace = true, features = ["force-inprocess"] } +hilog = "0.1.0" +napi-derive-ohos = "0.0.9" +napi-ohos = "0.1" +serde_json = { workspace = true } +surfman = { workspace = true, features = ["sm-angle-default"] } +webxr = { git = "https://github.com/servo/webxr" } +ohos-sys = { git = "https://github.com/jschwe/ohos-sys.git" } [target.'cfg(not(any(target_os = "android", target_env = "ohos")))'.dependencies] @@ -94,7 +102,6 @@ gleam = { workspace = true } glow = "0.13.1" keyboard-types = { workspace = true } raw-window-handle = "0.6" -servo-media = { workspace = true } shellwords = "1.0.0" surfman = { workspace = true, features = ["sm-x11", "sm-raw-window-handle-06"] } tinyfiledialogs = "3.0" diff --git a/ports/servoshell/backtrace.rs b/ports/servoshell/backtrace.rs index 7b7deeb1580..cfa1ada625a 100644 --- a/ports/servoshell/backtrace.rs +++ b/ports/servoshell/backtrace.rs @@ -24,6 +24,17 @@ pub(crate) fn print(w: &mut dyn std::io::Write) -> Result<(), std::io::Error> { ) } +#[cfg(target_env = "ohos")] +pub(crate) fn print_ohos() { + // Print to `hilog` + log::error!( + "{:?}", + Print { + print_fn_address: print as usize, + } + ) +} + struct Print { print_fn_address: usize, } diff --git a/ports/servoshell/build.rs b/ports/servoshell/build.rs index f226f2094b7..c593c3f8a88 100644 --- a/ports/servoshell/build.rs +++ b/ports/servoshell/build.rs @@ -10,6 +10,16 @@ use std::path::Path; use gl_generator::{Api, Fallbacks, Profile, Registry}; use vergen::EmitBuilder; +// We can make this configurable in the future if different platforms start to have +// different needs. +fn generate_egl_bindings(out_dir: &Path) { + let mut file = File::create(out_dir.join("egl_bindings.rs")).unwrap(); + Registry::new(Api::Egl, (1, 5), Profile::Core, Fallbacks::All, []) + .write_bindings(gl_generator::StaticStructGenerator, &mut file) + .unwrap(); + println!("cargo:rustc-link-lib=EGL"); +} + fn main() -> Result<(), Box<dyn Error>> { // Cargo does not expose the profile name to crates or their build scripts, // but we can extract it from OUT_DIR and set a custom cfg() ourselves. @@ -27,6 +37,7 @@ fn main() -> Result<(), Box<dyn Error>> { // Note: We can't use `#[cfg(windows)]`, since that would check the host platform // and not the target platform let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); + let target_env = std::env::var("CARGO_CFG_TARGET_ENV").unwrap(); if target_os == "windows" { #[cfg(windows)] @@ -43,12 +54,7 @@ fn main() -> Result<(), Box<dyn Error>> { .file("platform/macos/count_threads.c") .compile("count_threads"); } else if target_os == "android" { - // Generate GL bindings. For now, we only support EGL. - let mut file = File::create(out.join("egl_bindings.rs")).unwrap(); - Registry::new(Api::Egl, (1, 5), Profile::Core, Fallbacks::All, []) - .write_bindings(gl_generator::StaticStructGenerator, &mut file) - .unwrap(); - println!("cargo:rustc-link-lib=EGL"); + generate_egl_bindings(out); // FIXME: We need this workaround since jemalloc-sys still links // to libgcc instead of libunwind, but Android NDK 23c and above @@ -59,6 +65,8 @@ fn main() -> Result<(), Box<dyn Error>> { let mut libgcc = File::create(out.join("libgcc.a")).unwrap(); libgcc.write_all(b"INPUT(-lunwind)").unwrap(); println!("cargo:rustc-link-search=native={}", out.display()); + } else if target_env == "ohos" { + generate_egl_bindings(out); } if let Err(error) = EmitBuilder::builder() diff --git a/ports/servoshell/egl/android.rs b/ports/servoshell/egl/android.rs index eb8d08c8057..7c331febfee 100644 --- a/ports/servoshell/egl/android.rs +++ b/ports/servoshell/egl/android.rs @@ -18,11 +18,13 @@ use jni::{JNIEnv, JavaVM}; use libc::{dup2, pipe, read}; use log::{debug, error, info, warn}; use simpleservo::{ - Coordinates, DeviceIntRect, EventLoopWaker, HostTrait, InitOptions, InputMethodType, - MediaSessionPlaybackState, PromptResult, ServoGlue, SERVO, + DeviceIntRect, EventLoopWaker, InitOptions, InputMethodType, MediaSessionPlaybackState, + PromptResult, SERVO, }; use super::gl_glue; +use super::host_trait::HostTrait; +use super::servo_glue::{Coordinates, ServoGlue}; struct HostCallbacks { callbacks: GlobalRef, diff --git a/ports/servoshell/egl/android/simpleservo.rs b/ports/servoshell/egl/android/simpleservo.rs index 2e3824068a0..d12146382f5 100644 --- a/ports/servoshell/egl/android/simpleservo.rs +++ b/ports/servoshell/egl/android/simpleservo.rs @@ -6,51 +6,37 @@ use std::cell::RefCell; use std::collections::HashMap; use std::mem; use std::os::raw::c_void; -use std::path::PathBuf; use std::rc::Rc; use getopts::Options; -use ipc_channel::ipc::IpcSender; -use log::{debug, info, warn}; -use servo::base::id::WebViewId; -use servo::compositing::windowing::{ - AnimationState, EmbedderCoordinates, EmbedderEvent, EmbedderMethods, MouseWindowEvent, - WindowMethods, -}; +use log::info; +use servo::compositing::windowing::EmbedderEvent; use servo::compositing::CompositeTarget; use servo::config::prefs::pref_map; pub use servo::config::prefs::{add_user_prefs, PrefValue}; -use servo::embedder_traits::resources::{self, Resource, ResourceReaderMethods}; -pub use servo::embedder_traits::{ - ContextMenuResult, InputMethodType, MediaSessionPlaybackState, PermissionPrompt, - PermissionRequest, PromptResult, -}; -use servo::embedder_traits::{ - EmbedderMsg, EmbedderProxy, MediaSessionEvent, PromptDefinition, PromptOrigin, -}; -use servo::euclid::{Point2D, Rect, Scale, Size2D, Vector2D}; -use servo::keyboard_types::{Key, KeyState, KeyboardEvent}; -use servo::net_traits::pub_domains::is_reg_domain; -pub use servo::script_traits::{MediaSessionActionType, MouseButton}; -use servo::script_traits::{TouchEventType, TouchId, TraversalDirection}; +use servo::embedder_traits::resources; +/// The EventLoopWaker::wake function will be called from any thread. +/// It will be called to notify embedder that some events are available, +/// and that perform_updates need to be called +pub use servo::embedder_traits::EventLoopWaker; +pub use servo::embedder_traits::{InputMethodType, MediaSessionPlaybackState, PromptResult}; use servo::servo_config::{opts, pref}; use servo::servo_url::ServoUrl; pub use servo::webrender_api::units::DeviceIntRect; -use servo::webrender_api::units::DevicePixel; -use servo::webrender_api::ScrollLocation; use servo::webrender_traits::RenderingContext; -use servo::{self, gl, Servo, TopLevelBrowsingContextId}; +use servo::{self, gl, Servo}; use surfman::{Connection, SurfaceType}; +use crate::egl::host_trait::HostTrait; +use crate::egl::resources::ResourceReaderInstance; +use crate::egl::servo_glue::{ + Coordinates, ServoEmbedderCallbacks, ServoGlue, ServoWindowCallbacks, +}; + thread_local! { pub static SERVO: RefCell<Option<ServoGlue>> = RefCell::new(None); } -/// The EventLoopWaker::wake function will be called from any thread. -/// It will be called to notify embedder that some events are available, -/// and that perform_updates need to be called -pub use servo::embedder_traits::EventLoopWaker; - pub struct InitOptions { pub args: Vec<String>, pub url: Option<String>, @@ -69,121 +55,6 @@ pub enum SurfmanIntegration { Surface, } -#[derive(Clone, Debug)] -pub struct Coordinates { - pub viewport: Rect<i32, DevicePixel>, - pub framebuffer: Size2D<i32, DevicePixel>, -} - -impl Coordinates { - pub fn new( - x: i32, - y: i32, - width: i32, - height: i32, - fb_width: i32, - fb_height: i32, - ) -> Coordinates { - Coordinates { - viewport: Rect::new(Point2D::new(x, y), Size2D::new(width, height)), - framebuffer: Size2D::new(fb_width, fb_height), - } - } -} - -/// Callbacks. Implemented by embedder. Called by Servo. -pub trait HostTrait { - /// Show alert. - fn prompt_alert(&self, msg: String, trusted: bool); - /// Ask Yes/No question. - fn prompt_yes_no(&self, msg: String, trusted: bool) -> PromptResult; - /// Ask Ok/Cancel question. - fn prompt_ok_cancel(&self, msg: String, trusted: bool) -> PromptResult; - /// Ask for string - fn prompt_input(&self, msg: String, default: String, trusted: bool) -> Option<String>; - /// Show context menu - fn show_context_menu(&self, title: Option<String>, items: Vec<String>); - /// Page starts loading. - /// "Reload button" should be disabled. - /// "Stop button" should be enabled. - /// Throbber starts spinning. - fn on_load_started(&self); - /// Page has loaded. - /// "Reload button" should be enabled. - /// "Stop button" should be disabled. - /// Throbber stops spinning. - fn on_load_ended(&self); - /// Page title has changed. - fn on_title_changed(&self, title: Option<String>); - /// Allow Navigation. - fn on_allow_navigation(&self, url: String) -> bool; - /// Page URL has changed. - fn on_url_changed(&self, url: String); - /// Back/forward state has changed. - /// Back/forward buttons need to be disabled/enabled. - fn on_history_changed(&self, can_go_back: bool, can_go_forward: bool); - /// Page animation state has changed. If animating, it's recommended - /// that the embedder doesn't wait for the wake function to be called - /// to call perform_updates. Usually, it means doing: - /// while true { servo.perform_updates() }. This will end up calling flush - /// which will call swap_buffer which will be blocking long enough to limit - /// drawing at 60 FPS. - /// If not animating, call perform_updates only when needed (when the embedder - /// has events for Servo, or Servo has woken up the embedder event loop via - /// EventLoopWaker). - fn on_animating_changed(&self, animating: bool); - /// Servo finished shutting down. - fn on_shutdown_complete(&self); - /// A text input is focused. - fn on_ime_show( - &self, - input_type: InputMethodType, - text: Option<(String, i32)>, - multiline: bool, - bounds: DeviceIntRect, - ); - /// Input lost focus - fn on_ime_hide(&self); - /// Gets sytem clipboard contents. - fn get_clipboard_contents(&self) -> Option<String>; - /// Sets system clipboard contents. - fn set_clipboard_contents(&self, contents: String); - /// Called when we get the media session metadata/ - fn on_media_session_metadata(&self, title: String, artist: String, album: String); - /// Called when the media session playback state changes. - fn on_media_session_playback_state_change(&self, state: MediaSessionPlaybackState); - /// Called when the media session position state is set. - fn on_media_session_set_position_state(&self, duration: f64, position: f64, playback_rate: f64); - /// Called when devtools server is started - fn on_devtools_started(&self, port: Result<u16, ()>, token: String); - /// Called when we get a panic message from constellation - fn on_panic(&self, reason: String, backtrace: Option<String>); -} - -pub struct ServoGlue { - rendering_context: RenderingContext, - servo: Servo<ServoWindowCallbacks>, - batch_mode: bool, - callbacks: Rc<ServoWindowCallbacks>, - events: Vec<EmbedderEvent>, - context_menu_sender: Option<IpcSender<ContextMenuResult>>, - - /// List of top-level browsing contexts. - /// Modified by EmbedderMsg::WebViewOpened and EmbedderMsg::WebViewClosed, - /// and we exit if it ever becomes empty. - webviews: HashMap<WebViewId, WebView>, - - /// The order in which the webviews were created. - creation_order: Vec<WebViewId>, - - /// The webview that is currently focused. - /// Modified by EmbedderMsg::WebViewFocused and EmbedderMsg::WebViewBlurred. - focused_webview_id: Option<WebViewId>, -} - -#[derive(Debug)] -pub struct WebView {} - /// Test if a url is valid. pub fn is_uri_valid(url: &str) -> bool { info!("load_uri: {}", url); @@ -280,18 +151,18 @@ pub fn init( let rendering_context = RenderingContext::create(&connection, &adapter, surface_type) .or(Err("Failed to create surface manager"))?; - let window_callbacks = Rc::new(ServoWindowCallbacks { - host_callbacks: callbacks, - coordinates: RefCell::new(init_opts.coordinates), - density: init_opts.density, - rendering_context: rendering_context.clone(), - }); + let window_callbacks = Rc::new(ServoWindowCallbacks::new( + callbacks, + RefCell::new(init_opts.coordinates), + init_opts.density, + rendering_context.clone(), + )); - let embedder_callbacks = Box::new(ServoEmbedderCallbacks { - xr_discovery: init_opts.xr_discovery, + let embedder_callbacks = Box::new(ServoEmbedderCallbacks::new( waker, - gl: gl.clone(), - }); + init_opts.xr_discovery, + gl.clone(), + )); let servo = Servo::new( embedder_callbacks, @@ -301,17 +172,7 @@ pub fn init( ); SERVO.with(|s| { - let mut servo_glue = ServoGlue { - rendering_context, - servo: servo.servo, - batch_mode: false, - callbacks: window_callbacks, - events: vec![], - context_menu_sender: None, - webviews: HashMap::default(), - creation_order: vec![], - focused_webview_id: None, - }; + let mut servo_glue = ServoGlue::new(rendering_context, servo.servo, window_callbacks); let _ = servo_glue.process_event(EmbedderEvent::NewWebView(url, servo.browser_id)); *s.borrow_mut() = Some(servo_glue); }); @@ -322,626 +183,3 @@ pub fn init( pub fn deinit() { SERVO.with(|s| s.replace(None).unwrap().deinit()); } - -impl ServoGlue { - fn get_browser_id(&self) -> Result<TopLevelBrowsingContextId, &'static str> { - let webview_id = match self.focused_webview_id { - Some(id) => id, - None => return Err("No focused WebViewId yet."), - }; - Ok(webview_id) - } - - /// Request shutdown. Will call on_shutdown_complete. - pub fn request_shutdown(&mut self) -> Result<(), &'static str> { - self.process_event(EmbedderEvent::Quit) - } - - /// Call after on_shutdown_complete - pub fn deinit(self) { - self.servo.deinit(); - } - - /// Returns the webrender surface management integration interface. - /// This provides the embedder access to the current front buffer. - pub fn surfman(&self) -> RenderingContext { - self.rendering_context.clone() - } - - /// This is the Servo heartbeat. This needs to be called - /// everytime wakeup is called or when embedder wants Servo - /// to act on its pending events. - pub fn perform_updates(&mut self) -> Result<(), &'static str> { - debug!("perform_updates"); - let events = mem::replace(&mut self.events, Vec::new()); - self.servo.handle_events(events); - let r = self.handle_servo_events(); - debug!("done perform_updates"); - r - } - - /// In batch mode, Servo won't call perform_updates automatically. - /// This can be useful when the embedder wants to control when Servo - /// acts on its pending events. For example, if the embedder wants Servo - /// to act on the scroll events only at a certain time, not everytime - /// scroll() is called. - pub fn set_batch_mode(&mut self, batch: bool) -> Result<(), &'static str> { - debug!("set_batch_mode"); - self.batch_mode = batch; - Ok(()) - } - - /// Load an URL. - pub fn load_uri(&mut self, url: &str) -> Result<(), &'static str> { - info!("load_uri: {}", url); - crate::parser::location_bar_input_to_url(url) - .ok_or("Can't parse URL") - .and_then(|url| { - let browser_id = self.get_browser_id()?; - let event = EmbedderEvent::LoadUrl(browser_id, url); - self.process_event(event) - }) - } - - /// Reload the page. - pub fn clear_cache(&mut self) -> Result<(), &'static str> { - info!("clear_cache"); - let event = EmbedderEvent::ClearCache; - self.process_event(event) - } - - /// Reload the page. - pub fn reload(&mut self) -> Result<(), &'static str> { - info!("reload"); - let browser_id = self.get_browser_id()?; - let event = EmbedderEvent::Reload(browser_id); - self.process_event(event) - } - - /// Redraw the page. - pub fn refresh(&mut self) -> Result<(), &'static str> { - info!("refresh"); - self.process_event(EmbedderEvent::Refresh) - } - - /// Stop loading the page. - pub fn stop(&mut self) -> Result<(), &'static str> { - warn!("TODO can't stop won't stop"); - Ok(()) - } - - /// Go back in history. - pub fn go_back(&mut self) -> Result<(), &'static str> { - info!("go_back"); - let browser_id = self.get_browser_id()?; - let event = EmbedderEvent::Navigation(browser_id, TraversalDirection::Back(1)); - self.process_event(event) - } - - /// Go forward in history. - pub fn go_forward(&mut self) -> Result<(), &'static str> { - info!("go_forward"); - let browser_id = self.get_browser_id()?; - let event = EmbedderEvent::Navigation(browser_id, TraversalDirection::Forward(1)); - self.process_event(event) - } - - /// Let Servo know that the window has been resized. - pub fn resize(&mut self, coordinates: Coordinates) -> Result<(), &'static str> { - info!("resize"); - *self.callbacks.coordinates.borrow_mut() = coordinates; - self.process_event(EmbedderEvent::WindowResize) - } - - /// Start scrolling. - /// x/y are scroll coordinates. - /// dx/dy are scroll deltas. - pub fn scroll_start(&mut self, dx: f32, dy: f32, x: i32, y: i32) -> Result<(), &'static str> { - let delta = Vector2D::new(dx, dy); - let scroll_location = ScrollLocation::Delta(delta); - let event = - EmbedderEvent::Scroll(scroll_location, Point2D::new(x, y), TouchEventType::Down); - self.process_event(event) - } - - /// Scroll. - /// x/y are scroll coordinates. - /// dx/dy are scroll deltas. - pub fn scroll(&mut self, dx: f32, dy: f32, x: i32, y: i32) -> Result<(), &'static str> { - let delta = Vector2D::new(dx, dy); - let scroll_location = ScrollLocation::Delta(delta); - let event = - EmbedderEvent::Scroll(scroll_location, Point2D::new(x, y), TouchEventType::Move); - self.process_event(event) - } - - /// End scrolling. - /// x/y are scroll coordinates. - /// dx/dy are scroll deltas. - pub fn scroll_end(&mut self, dx: f32, dy: f32, x: i32, y: i32) -> Result<(), &'static str> { - let delta = Vector2D::new(dx, dy); - let scroll_location = ScrollLocation::Delta(delta); - let event = EmbedderEvent::Scroll(scroll_location, Point2D::new(x, y), TouchEventType::Up); - self.process_event(event) - } - - /// Touch event: press down - pub fn touch_down(&mut self, x: f32, y: f32, pointer_id: i32) -> Result<(), &'static str> { - let event = EmbedderEvent::Touch( - TouchEventType::Down, - TouchId(pointer_id), - Point2D::new(x as f32, y as f32), - ); - self.process_event(event) - } - - /// Touch event: move touching finger - pub fn touch_move(&mut self, x: f32, y: f32, pointer_id: i32) -> Result<(), &'static str> { - let event = EmbedderEvent::Touch( - TouchEventType::Move, - TouchId(pointer_id), - Point2D::new(x as f32, y as f32), - ); - self.process_event(event) - } - - /// Touch event: Lift touching finger - pub fn touch_up(&mut self, x: f32, y: f32, pointer_id: i32) -> Result<(), &'static str> { - let event = EmbedderEvent::Touch( - TouchEventType::Up, - TouchId(pointer_id), - Point2D::new(x as f32, y as f32), - ); - self.process_event(event) - } - - /// Cancel touch event - pub fn touch_cancel(&mut self, x: f32, y: f32, pointer_id: i32) -> Result<(), &'static str> { - let event = EmbedderEvent::Touch( - TouchEventType::Cancel, - TouchId(pointer_id), - Point2D::new(x as f32, y as f32), - ); - self.process_event(event) - } - - /// Register a mouse movement. - pub fn mouse_move(&mut self, x: f32, y: f32) -> Result<(), &'static str> { - let point = Point2D::new(x, y); - let event = EmbedderEvent::MouseWindowMoveEventClass(point); - self.process_event(event) - } - - /// Register a mouse button press. - pub fn mouse_down(&mut self, x: f32, y: f32, button: MouseButton) -> Result<(), &'static str> { - let point = Point2D::new(x, y); - let event = - EmbedderEvent::MouseWindowEventClass(MouseWindowEvent::MouseDown(button, point)); - self.process_event(event) - } - - /// Register a mouse button release. - pub fn mouse_up(&mut self, x: f32, y: f32, button: MouseButton) -> Result<(), &'static str> { - let point = Point2D::new(x, y); - let event = EmbedderEvent::MouseWindowEventClass(MouseWindowEvent::MouseUp(button, point)); - self.process_event(event) - } - - /// Start pinchzoom. - /// x/y are pinch origin coordinates. - pub fn pinchzoom_start(&mut self, factor: f32, _x: u32, _y: u32) -> Result<(), &'static str> { - self.process_event(EmbedderEvent::PinchZoom(factor)) - } - - /// Pinchzoom. - /// x/y are pinch origin coordinates. - pub fn pinchzoom(&mut self, factor: f32, _x: u32, _y: u32) -> Result<(), &'static str> { - self.process_event(EmbedderEvent::PinchZoom(factor)) - } - - /// End pinchzoom. - /// x/y are pinch origin coordinates. - pub fn pinchzoom_end(&mut self, factor: f32, _x: u32, _y: u32) -> Result<(), &'static str> { - self.process_event(EmbedderEvent::PinchZoom(factor)) - } - - /// Perform a click. - pub fn click(&mut self, x: f32, y: f32) -> Result<(), &'static str> { - let mouse_event = MouseWindowEvent::Click(MouseButton::Left, Point2D::new(x, y)); - let event = EmbedderEvent::MouseWindowEventClass(mouse_event); - self.process_event(event) - } - - pub fn key_down(&mut self, key: Key) -> Result<(), &'static str> { - let key_event = KeyboardEvent { - state: KeyState::Down, - key, - ..KeyboardEvent::default() - }; - self.process_event(EmbedderEvent::Keyboard(key_event)) - } - - pub fn key_up(&mut self, key: Key) -> Result<(), &'static str> { - let key_event = KeyboardEvent { - state: KeyState::Up, - key, - ..KeyboardEvent::default() - }; - self.process_event(EmbedderEvent::Keyboard(key_event)) - } - - pub fn pause_compositor(&mut self) -> Result<(), &'static str> { - self.process_event(EmbedderEvent::InvalidateNativeSurface) - } - - pub fn resume_compositor( - &mut self, - native_surface: *mut c_void, - coords: Coordinates, - ) -> Result<(), &'static str> { - if native_surface.is_null() { - panic!("null passed for native_surface"); - } - self.process_event(EmbedderEvent::ReplaceNativeSurface( - native_surface, - coords.framebuffer, - )) - } - - pub fn media_session_action( - &mut self, - action: MediaSessionActionType, - ) -> Result<(), &'static str> { - info!("Media session action {:?}", action); - self.process_event(EmbedderEvent::MediaSessionAction(action)) - } - - pub fn set_throttled(&mut self, throttled: bool) -> Result<(), &'static str> { - info!("set_throttled"); - if let Ok(id) = self.get_browser_id() { - let event = EmbedderEvent::SetWebViewThrottled(id, throttled); - self.process_event(event) - } else { - // Ignore visibility change if no browser has been created yet. - Ok(()) - } - } - - pub fn ime_dismissed(&mut self) -> Result<(), &'static str> { - info!("ime_dismissed"); - self.process_event(EmbedderEvent::IMEDismissed) - } - - pub fn on_context_menu_closed( - &mut self, - result: ContextMenuResult, - ) -> Result<(), &'static str> { - if let Some(sender) = self.context_menu_sender.take() { - let _ = sender.send(result); - } else { - warn!("Trying to close a context menu when no context menu is active"); - } - Ok(()) - } - - fn process_event(&mut self, event: EmbedderEvent) -> Result<(), &'static str> { - self.events.push(event); - if !self.batch_mode { - self.perform_updates() - } else { - Ok(()) - } - } - - fn handle_servo_events(&mut self) -> Result<(), &'static str> { - let mut need_update = false; - let mut need_present = false; - for (browser_id, event) in self.servo.get_events() { - match event { - EmbedderMsg::ChangePageTitle(title) => { - self.callbacks.host_callbacks.on_title_changed(title); - }, - EmbedderMsg::AllowNavigationRequest(pipeline_id, url) => { - if let Some(_browser_id) = browser_id { - let data: bool = self - .callbacks - .host_callbacks - .on_allow_navigation(url.to_string()); - let window_event = - EmbedderEvent::AllowNavigationResponse(pipeline_id, data); - self.events.push(window_event); - need_update = true; - } - }, - EmbedderMsg::HistoryChanged(entries, current) => { - let can_go_back = current > 0; - let can_go_forward = current < entries.len() - 1; - self.callbacks - .host_callbacks - .on_history_changed(can_go_back, can_go_forward); - self.callbacks - .host_callbacks - .on_url_changed(entries[current].clone().to_string()); - }, - EmbedderMsg::LoadStart => { - self.callbacks.host_callbacks.on_load_started(); - }, - EmbedderMsg::LoadComplete => { - self.callbacks.host_callbacks.on_load_ended(); - }, - EmbedderMsg::GetSelectedBluetoothDevice(_, sender) => { - let _ = sender.send(None); - }, - EmbedderMsg::AllowUnload(sender) => { - let _ = sender.send(true); - }, - EmbedderMsg::ShowContextMenu(sender, title, items) => { - if self.context_menu_sender.is_some() { - warn!( - "Trying to show a context menu when a context menu is already active" - ); - let _ = sender.send(ContextMenuResult::Ignored); - } else { - self.context_menu_sender = Some(sender); - self.callbacks - .host_callbacks - .show_context_menu(title, items); - } - }, - EmbedderMsg::Prompt(definition, origin) => { - let cb = &self.callbacks.host_callbacks; - let trusted = origin == PromptOrigin::Trusted; - let res = match definition { - PromptDefinition::Alert(message, sender) => { - sender.send(cb.prompt_alert(message, trusted)) - }, - PromptDefinition::OkCancel(message, sender) => { - sender.send(cb.prompt_ok_cancel(message, trusted)) - }, - PromptDefinition::YesNo(message, sender) => { - sender.send(cb.prompt_yes_no(message, trusted)) - }, - PromptDefinition::Input(message, default, sender) => { - sender.send(cb.prompt_input(message, default, trusted)) - }, - }; - if let Err(e) = res { - let reason = format!("Failed to send Prompt response: {}", e); - self.events - .push(EmbedderEvent::SendError(browser_id, reason)); - } - }, - EmbedderMsg::AllowOpeningWebView(response_chan) => { - // Note: would be a place to handle pop-ups config. - // see Step 7 of #the-rules-for-choosing-a-browsing-context-given-a-browsing-context-name - if let Err(e) = response_chan.send(true) { - warn!("Failed to send AllowOpeningBrowser response: {}", e); - }; - }, - EmbedderMsg::WebViewOpened(new_webview_id) => { - self.webviews.insert(new_webview_id, WebView {}); - self.creation_order.push(new_webview_id); - self.events - .push(EmbedderEvent::FocusWebView(new_webview_id)); - }, - EmbedderMsg::WebViewClosed(webview_id) => { - self.webviews.retain(|&id, _| id != webview_id); - self.creation_order.retain(|&id| id != webview_id); - self.focused_webview_id = None; - if let Some(&newest_webview_id) = self.creation_order.last() { - self.events - .push(EmbedderEvent::FocusWebView(newest_webview_id)); - } else { - self.events.push(EmbedderEvent::Quit); - } - }, - EmbedderMsg::WebViewFocused(webview_id) => { - self.focused_webview_id = Some(webview_id); - }, - EmbedderMsg::WebViewBlurred => { - self.focused_webview_id = None; - }, - EmbedderMsg::GetClipboardContents(sender) => { - let contents = self.callbacks.host_callbacks.get_clipboard_contents(); - let _ = sender.send(contents.unwrap_or("".to_owned())); - }, - EmbedderMsg::SetClipboardContents(text) => { - self.callbacks.host_callbacks.set_clipboard_contents(text); - }, - EmbedderMsg::Shutdown => { - self.callbacks.host_callbacks.on_shutdown_complete(); - }, - EmbedderMsg::PromptPermission(prompt, sender) => { - let message = match prompt { - PermissionPrompt::Request(permission_name) => { - format!("Do you want to grant permission for {:?}?", permission_name) - }, - PermissionPrompt::Insecure(permission_name) => { - format!( - "The {:?} feature is only safe to use in secure context, but servo can't guarantee\n\ - that the current context is secure. Do you want to proceed and grant permission?", - permission_name - ) - }, - }; - - let result = match self.callbacks.host_callbacks.prompt_yes_no(message, true) { - PromptResult::Primary => PermissionRequest::Granted, - PromptResult::Secondary | PromptResult::Dismissed => { - PermissionRequest::Denied - }, - }; - - let _ = sender.send(result); - }, - EmbedderMsg::ShowIME(kind, text, multiline, bounds) => { - self.callbacks - .host_callbacks - .on_ime_show(kind, text, multiline, bounds); - }, - EmbedderMsg::HideIME => { - self.callbacks.host_callbacks.on_ime_hide(); - }, - EmbedderMsg::MediaSessionEvent(event) => { - match event { - MediaSessionEvent::SetMetadata(metadata) => { - self.callbacks.host_callbacks.on_media_session_metadata( - metadata.title, - metadata.artist, - metadata.album, - ) - }, - MediaSessionEvent::PlaybackStateChange(state) => self - .callbacks - .host_callbacks - .on_media_session_playback_state_change(state), - MediaSessionEvent::SetPositionState(position_state) => self - .callbacks - .host_callbacks - .on_media_session_set_position_state( - position_state.duration, - position_state.position, - position_state.playback_rate, - ), - }; - }, - EmbedderMsg::OnDevtoolsStarted(port, token) => { - self.callbacks - .host_callbacks - .on_devtools_started(port, token); - }, - EmbedderMsg::Panic(reason, backtrace) => { - self.callbacks.host_callbacks.on_panic(reason, backtrace); - }, - EmbedderMsg::ReadyToPresent(_webview_ids) => { - need_present = true; - }, - EmbedderMsg::Status(..) | - EmbedderMsg::SelectFiles(..) | - EmbedderMsg::MoveTo(..) | - EmbedderMsg::ResizeTo(..) | - EmbedderMsg::Keyboard(..) | - EmbedderMsg::SetCursor(..) | - EmbedderMsg::NewFavicon(..) | - EmbedderMsg::HeadParsed | - EmbedderMsg::SetFullscreenState(..) | - EmbedderMsg::ReportProfile(..) | - EmbedderMsg::EventDelivered(..) => {}, - } - } - - if need_update { - let _ = self.perform_updates(); - } - if need_present { - self.servo.present(); - } - Ok(()) - } -} - -struct ServoEmbedderCallbacks { - waker: Box<dyn EventLoopWaker>, - xr_discovery: Option<webxr::Discovery>, - #[allow(unused)] - gl: Rc<dyn gl::Gl>, -} - -struct ServoWindowCallbacks { - host_callbacks: Box<dyn HostTrait>, - coordinates: RefCell<Coordinates>, - density: f32, - rendering_context: RenderingContext, -} - -impl EmbedderMethods for ServoEmbedderCallbacks { - fn register_webxr( - &mut self, - registry: &mut webxr::MainThreadRegistry, - _embedder_proxy: EmbedderProxy, - ) { - debug!("EmbedderMethods::register_xr"); - if let Some(discovery) = self.xr_discovery.take() { - registry.register(discovery); - } - } - - fn create_event_loop_waker(&mut self) -> Box<dyn EventLoopWaker> { - debug!("EmbedderMethods::create_event_loop_waker"); - self.waker.clone() - } -} - -impl WindowMethods for ServoWindowCallbacks { - fn rendering_context(&self) -> RenderingContext { - self.rendering_context.clone() - } - - fn set_animation_state(&self, state: AnimationState) { - debug!("WindowMethods::set_animation_state: {:?}", state); - self.host_callbacks - .on_animating_changed(state == AnimationState::Animating); - } - - fn get_coordinates(&self) -> EmbedderCoordinates { - let coords = self.coordinates.borrow(); - EmbedderCoordinates { - viewport: coords.viewport.to_box2d(), - framebuffer: coords.framebuffer, - window: (coords.viewport.size, Point2D::new(0, 0)), - screen: coords.viewport.size, - screen_avail: coords.viewport.size, - hidpi_factor: Scale::new(self.density), - } - } -} - -struct ResourceReaderInstance; - -impl ResourceReaderInstance { - fn new() -> ResourceReaderInstance { - ResourceReaderInstance - } -} - -impl ResourceReaderMethods for ResourceReaderInstance { - fn read(&self, res: Resource) -> Vec<u8> { - Vec::from(match res { - Resource::Preferences => &include_bytes!("../../../../resources/prefs.json")[..], - Resource::HstsPreloadList => { - &include_bytes!("../../../../resources/hsts_preload.json")[..] - }, - Resource::BadCertHTML => &include_bytes!("../../../../resources/badcert.html")[..], - Resource::NetErrorHTML => &include_bytes!("../../../../resources/neterror.html")[..], - Resource::UserAgentCSS => &include_bytes!("../../../../resources/user-agent.css")[..], - Resource::ServoCSS => &include_bytes!("../../../../resources/servo.css")[..], - Resource::PresentationalHintsCSS => { - &include_bytes!("../../../../resources/presentational-hints.css")[..] - }, - Resource::QuirksModeCSS => &include_bytes!("../../../../resources/quirks-mode.css")[..], - Resource::RippyPNG => &include_bytes!("../../../../resources/rippy.png")[..], - Resource::DomainList => &include_bytes!("../../../../resources/public_domains.txt")[..], - Resource::BluetoothBlocklist => { - &include_bytes!("../../../../resources/gatt_blocklist.txt")[..] - }, - Resource::MediaControlsCSS => { - &include_bytes!("../../../../resources/media-controls.css")[..] - }, - Resource::MediaControlsJS => { - &include_bytes!("../../../../resources/media-controls.js")[..] - }, - Resource::CrashHTML => &include_bytes!("../../../../resources/crash.html")[..], - Resource::DirectoryListingHTML => { - &include_bytes!("../../../../resources/directory-listing.html")[..] - }, - }) - } - - fn sandbox_access_files(&self) -> Vec<PathBuf> { - vec![] - } - - fn sandbox_access_files_dirs(&self) -> Vec<PathBuf> { - vec![] - } -} diff --git a/ports/servoshell/egl/gl_glue.rs b/ports/servoshell/egl/gl_glue.rs index 78a44592c7a..e0c9a3c564c 100644 --- a/ports/servoshell/egl/gl_glue.rs +++ b/ports/servoshell/egl/gl_glue.rs @@ -2,6 +2,7 @@ * 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_camel_case_types)] +#![allow(unused_imports)] pub type ServoGl = std::rc::Rc<dyn servo::gl::Gl>; diff --git a/ports/servoshell/egl/host_trait.rs b/ports/servoshell/egl/host_trait.rs new file mode 100644 index 00000000000..361d431d237 --- /dev/null +++ b/ports/servoshell/egl/host_trait.rs @@ -0,0 +1,75 @@ +/* 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 servo::embedder_traits::{InputMethodType, MediaSessionPlaybackState, PromptResult}; +use servo::webrender_api::units::DeviceIntRect; + +/// Callbacks. Implemented by embedder. Called by Servo. +pub trait HostTrait { + /// Show alert. + fn prompt_alert(&self, msg: String, trusted: bool); + /// Ask Yes/No question. + fn prompt_yes_no(&self, msg: String, trusted: bool) -> PromptResult; + /// Ask Ok/Cancel question. + fn prompt_ok_cancel(&self, msg: String, trusted: bool) -> PromptResult; + /// Ask for string + fn prompt_input(&self, msg: String, default: String, trusted: bool) -> Option<String>; + /// Show context menu + fn show_context_menu(&self, title: Option<String>, items: Vec<String>); + /// Page starts loading. + /// "Reload button" should be disabled. + /// "Stop button" should be enabled. + /// Throbber starts spinning. + fn on_load_started(&self); + /// Page has loaded. + /// "Reload button" should be enabled. + /// "Stop button" should be disabled. + /// Throbber stops spinning. + fn on_load_ended(&self); + /// Page title has changed. + fn on_title_changed(&self, title: Option<String>); + /// Allow Navigation. + fn on_allow_navigation(&self, url: String) -> bool; + /// Page URL has changed. + fn on_url_changed(&self, url: String); + /// Back/forward state has changed. + /// Back/forward buttons need to be disabled/enabled. + fn on_history_changed(&self, can_go_back: bool, can_go_forward: bool); + /// Page animation state has changed. If animating, it's recommended + /// that the embedder doesn't wait for the wake function to be called + /// to call perform_updates. Usually, it means doing: + /// while true { servo.perform_updates() }. This will end up calling flush + /// which will call swap_buffer which will be blocking long enough to limit + /// drawing at 60 FPS. + /// If not animating, call perform_updates only when needed (when the embedder + /// has events for Servo, or Servo has woken up the embedder event loop via + /// EventLoopWaker). + fn on_animating_changed(&self, animating: bool); + /// Servo finished shutting down. + fn on_shutdown_complete(&self); + /// A text input is focused. + fn on_ime_show( + &self, + input_type: InputMethodType, + text: Option<(String, i32)>, + multiline: bool, + bounds: DeviceIntRect, + ); + /// Input lost focus + fn on_ime_hide(&self); + /// Gets sytem clipboard contents. + fn get_clipboard_contents(&self) -> Option<String>; + /// Sets system clipboard contents. + fn set_clipboard_contents(&self, contents: String); + /// Called when we get the media session metadata/ + fn on_media_session_metadata(&self, title: String, artist: String, album: String); + /// Called when the media session playback state changes. + fn on_media_session_playback_state_change(&self, state: MediaSessionPlaybackState); + /// Called when the media session position state is set. + fn on_media_session_set_position_state(&self, duration: f64, position: f64, playback_rate: f64); + /// Called when devtools server is started + fn on_devtools_started(&self, port: Result<u16, ()>, token: String); + /// Called when we get a panic message from constellation + fn on_panic(&self, reason: String, backtrace: Option<String>); +} diff --git a/ports/servoshell/egl/mod.rs b/ports/servoshell/egl/mod.rs index 80c34a5bbb9..23c856d2dee 100644 --- a/ports/servoshell/egl/mod.rs +++ b/ports/servoshell/egl/mod.rs @@ -2,8 +2,15 @@ * 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/. */ -#[cfg(target_os = "android")] +#[cfg(any(target_os = "android", target_env = "ohos"))] pub mod gl_glue; #[cfg(target_os = "android")] mod android; + +#[cfg(target_env = "ohos")] +mod ohos; + +mod host_trait; +mod resources; +mod servo_glue; diff --git a/ports/servoshell/egl/ohos.rs b/ports/servoshell/egl/ohos.rs new file mode 100644 index 00000000000..3b7caa5c633 --- /dev/null +++ b/ports/servoshell/egl/ohos.rs @@ -0,0 +1,619 @@ +/* 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/. */ +#![allow(non_snake_case)] + +use std::mem::MaybeUninit; +use std::os::raw::c_void; +use std::sync::mpsc::{Receiver, Sender}; +use std::sync::{mpsc, Once, OnceLock}; +use std::thread; +use std::thread::sleep; +use std::time::Duration; + +use log::{debug, error, info, warn, LevelFilter}; +use napi_derive_ohos::{module_exports, napi}; +use napi_ohos::bindgen_prelude::Undefined; +use napi_ohos::threadsafe_function::{ + ErrorStrategy, ThreadsafeFunction, ThreadsafeFunctionCallMode, +}; +use napi_ohos::{Env, JsFunction, JsObject, JsString, NapiRaw}; +use ohos_sys::ace::xcomponent::native_interface_xcomponent::{ + OH_NativeXComponent, OH_NativeXComponent_Callback, OH_NativeXComponent_GetTouchEvent, + OH_NativeXComponent_TouchEvent, OH_NativeXComponent_TouchEventType, +}; +use servo::embedder_traits::PromptResult; +use servo::euclid::Point2D; +use servo::style::Zero; +use simpleservo::EventLoopWaker; + +use super::gl_glue; +use super::host_trait::HostTrait; +use super::servo_glue::ServoGlue; + +mod simpleservo; + +// Todo: in the future these libraries should be added by Rust sys-crates +#[link(name = "ace_napi.z")] +#[link(name = "ace_ndk.z")] +#[link(name = "hilog_ndk.z")] +#[link(name = "native_window")] +#[link(name = "clang_rt.builtins", kind = "static")] +extern "C" {} + +#[napi(object)] +#[derive(Debug)] +pub struct InitOpts { + pub url: String, + pub device_type: String, + pub os_full_name: String, + pub display_density: f64, +} + +#[derive(Debug)] +enum CallError { + ChannelNotInitialized, + ChannelDied, +} + +fn call(action: ServoAction) -> Result<(), CallError> { + let tx = SERVO_CHANNEL + .get() + .ok_or(CallError::ChannelNotInitialized)?; + tx.send(action).map_err(|_| CallError::ChannelDied)?; + Ok(()) +} + +#[repr(transparent)] +struct XComponentWrapper(*mut OH_NativeXComponent); +#[repr(transparent)] +struct WindowWrapper(*mut c_void); +unsafe impl Send for XComponentWrapper {} +unsafe impl Send for WindowWrapper {} + +#[derive(Clone, Copy, Debug)] +enum TouchEventType { + Down, + Up, + Move, + Scroll { dx: f32, dy: f32 }, + Cancel, + Unknown, +} + +#[derive(Debug)] +enum ServoAction { + WakeUp, + LoadUrl(String), + TouchEvent { + kind: TouchEventType, + x: f32, + y: f32, + pointer_id: i32, + }, + Initialize(Box<InitOpts>), +} + +#[derive(Clone, Copy, Debug, Default)] +enum Direction2D { + Horizontal, + Vertical, + #[default] + Free, +} +#[derive(Clone, Debug)] +struct TouchTracker { + last_position: Point2D<f32, f32>, +} + +impl TouchTracker { + fn new(first_point: Point2D<f32, f32>) -> Self { + TouchTracker { + last_position: first_point, + } + } +} + +// Todo: Need to check if OnceLock is suitable, or if the TS function can be destroyed, e.g. +// if the activity gets suspended. +static SET_URL_BAR_CB: OnceLock<ThreadsafeFunction<String, ErrorStrategy::Fatal>> = OnceLock::new(); + +struct TsThreadState { + // last_touch_event: Option<OH_NativeXComponent_TouchEvent>, + velocity_tracker: Option<TouchTracker>, +} + +impl TsThreadState { + const fn new() -> Self { + Self { + velocity_tracker: None, + } + } +} + +static mut TS_THREAD_STATE: TsThreadState = TsThreadState::new(); + +impl ServoAction { + fn dispatch_touch_event( + servo: &mut ServoGlue, + kind: TouchEventType, + x: f32, + y: f32, + pointer_id: i32, + ) -> Result<(), &'static str> { + match kind { + TouchEventType::Down => servo.touch_down(x, y, pointer_id), + TouchEventType::Up => servo.touch_up(x, y, pointer_id), + TouchEventType::Scroll { dx, dy } => servo.scroll(dx, dy, x as i32, y as i32), + TouchEventType::Move => servo.touch_move(x, y, pointer_id), + TouchEventType::Cancel => servo.touch_cancel(x, y, pointer_id), + TouchEventType::Unknown => Err("Can't dispatch Unknown Touch Event"), + } + } + + fn do_action(&self, servo: &mut ServoGlue) { + use ServoAction::*; + let res = match self { + WakeUp => servo.perform_updates(), + LoadUrl(url) => servo.load_uri(url.as_str()), + TouchEvent { + kind, + x, + y, + pointer_id, + } => Self::dispatch_touch_event(servo, *kind, *x, *y, *pointer_id), + Initialize(_init_opts) => { + panic!("Received Initialize event, even though servo is already initialized") + }, + }; + if let Err(e) = res { + error!("Failed to do {self:?} with error {e}"); + } + } +} + +static SERVO_CHANNEL: OnceLock<Sender<ServoAction>> = OnceLock::new(); + +#[no_mangle] +pub extern "C" fn on_surface_created_cb(xcomponent: *mut OH_NativeXComponent, window: *mut c_void) { + info!("on_surface_created_cb"); + + let xc_wrapper = XComponentWrapper(xcomponent); + let window_wrapper = WindowWrapper(window); + + // 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"); + + let wakeup = Box::new(WakeupCallback::new(tx)); + let callbacks = Box::new(HostCallbacks::new()); + + let egl_init = gl_glue::init().expect("egl::init() failed"); + 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 mut servo = simpleservo::init( + *init_opts, + window.0, + xc.0, + egl_init.gl_wrapper, + wakeup, + callbacks, + ) + .expect("Servo initialization failed"); + + info!("Surface created!"); + + while let Ok(action) = rx.recv() { + info!("Wakeup message received!"); + action.do_action(&mut servo); + } + + info!("Sender disconnected - Terminating main surface thread"); + }); + + info!("Returning from on_surface_created_cb"); +} + +// Todo: Probably we need to block here, until the main thread has processed the change. +pub extern "C" fn on_surface_changed_cb( + _component: *mut OH_NativeXComponent, + _window: *mut c_void, +) { + error!("on_surface_changed_cb is currently not implemented!"); +} + +pub extern "C" fn on_surface_destroyed_cb( + _component: *mut OH_NativeXComponent, + _window: *mut c_void, +) { + error!("on_surface_destroyed_cb is currently not implemented"); +} + +pub extern "C" fn on_dispatch_touch_event_cb( + component: *mut OH_NativeXComponent, + window: *mut c_void, +) { + info!("DispatchTouchEvent"); + let mut touch_event: MaybeUninit<OH_NativeXComponent_TouchEvent> = MaybeUninit::uninit(); + let res = + unsafe { OH_NativeXComponent_GetTouchEvent(component, window, touch_event.as_mut_ptr()) }; + if res != 0 { + error!("OH_NativeXComponent_GetTouchEvent failed with {res}"); + return; + } + let touch_event = unsafe { touch_event.assume_init() }; + let kind: TouchEventType = + match touch_event.type_ { + OH_NativeXComponent_TouchEventType::OH_NATIVEXCOMPONENT_DOWN => { + if touch_event.id == 0 { + unsafe { + let old = TS_THREAD_STATE.velocity_tracker.replace(TouchTracker::new( + Point2D::new(touch_event.x, touch_event.y), + )); + assert!(old.is_none()); + } + } + TouchEventType::Down + }, + OH_NativeXComponent_TouchEventType::OH_NATIVEXCOMPONENT_UP => { + if touch_event.id == 0 { + unsafe { + let old = TS_THREAD_STATE.velocity_tracker.take(); + assert!(old.is_some()); + } + } + TouchEventType::Up + }, + OH_NativeXComponent_TouchEventType::OH_NATIVEXCOMPONENT_MOVE => { + // SAFETY: We only access TS_THREAD_STATE from the main TS thread. + if touch_event.id == 0 { + let (lastX, lastY) = unsafe { + if let Some(last_event) = &mut TS_THREAD_STATE.velocity_tracker { + let touch_point = last_event.last_position; + last_event.last_position = Point2D::new(touch_event.x, touch_event.y); + (touch_point.x, touch_point.y) + } else { + error!("Move Event received, but no previous touch event was stored!"); + // todo: handle this error case + panic!("Move Event received, but no previous touch event was stored!"); + } + }; + let dx = touch_event.x - lastX; + let dy = touch_event.y - lastY; + TouchEventType::Scroll { dx, dy } + } else { + TouchEventType::Move + } + }, + OH_NativeXComponent_TouchEventType::OH_NATIVEXCOMPONENT_CANCEL => { + if touch_event.id == 0 { + unsafe { + let old = TS_THREAD_STATE.velocity_tracker.take(); + assert!(old.is_some()); + } + } + TouchEventType::Cancel + }, + _ => { + error!( + "Failed to dispatch call for touch Event {:?}", + touch_event.type_ + ); + TouchEventType::Unknown + }, + }; + if let Err(e) = call(ServoAction::TouchEvent { + kind, + x: touch_event.x, + y: touch_event.y, + pointer_id: touch_event.id, + }) { + error!("Failed to dispatch call for touch Event {kind:?}: {e:?}"); + } +} + +fn initialize_logging_once() { + static ONCE: Once = Once::new(); + ONCE.call_once(|| { + let mut builder = hilog::Builder::new(); + builder.filter_level(LevelFilter::Debug).init(); + + info!("Servo Register callback called!"); + + std::panic::set_hook(Box::new(|info| { + error!("Panic in Rust code"); + error!("PanicInfo: {info}"); + let msg = match info.payload().downcast_ref::<&'static str>() { + Some(s) => *s, + None => match info.payload().downcast_ref::<String>() { + Some(s) => &**s, + None => "Box<Any>", + }, + }; + let current_thread = thread::current(); + let name = current_thread.name().unwrap_or("<unnamed>"); + if let Some(location) = info.location() { + let _ = error!( + "{} (thread {}, at {}:{})", + msg, + name, + location.file(), + location.line() + ); + } else { + let _ = error!("{} (thread {})", msg, name); + } + + let _ = crate::backtrace::print_ohos(); + })); + }) +} + +fn register_xcomponent_callbacks(env: &Env, xcomponent: &JsObject) -> napi_ohos::Result<()> { + info!("napi_get_named_property call successfull"); + let raw = unsafe { xcomponent.raw() }; + let raw_env = env.raw(); + let mut nativeXComponent: *mut OH_NativeXComponent = core::ptr::null_mut(); + unsafe { + let res = napi_ohos::sys::napi_unwrap( + raw_env, + raw, + &mut nativeXComponent as *mut *mut OH_NativeXComponent as *mut *mut c_void, + ); + assert!(res.is_zero()); + } + info!("Got nativeXComponent!"); + let cbs = Box::new(OH_NativeXComponent_Callback { + OnSurfaceCreated: Some(on_surface_created_cb), + OnSurfaceChanged: Some(on_surface_changed_cb), + OnSurfaceDestroyed: Some(on_surface_destroyed_cb), + DispatchTouchEvent: Some(on_dispatch_touch_event_cb), + }); + use ohos_sys::ace::xcomponent::native_interface_xcomponent::OH_NativeXComponent_RegisterCallback; + let res = + unsafe { OH_NativeXComponent_RegisterCallback(nativeXComponent, Box::leak(cbs) as *mut _) }; + if res != 0 { + error!("Failed to register callbacks"); + } else { + info!("Registerd callbacks successfully"); + } + Ok(()) +} + +#[allow(unused)] +fn debug_jsobject(obj: &JsObject, obj_name: &str) -> napi_ohos::Result<()> { + let names = obj.get_property_names()?; + error!("Getting property names of object {obj_name}"); + let len = names.get_array_length()?; + error!("{obj_name} has {len} elements"); + for i in 0..len { + let name: JsString = names.get_element(i)?; + let name = name.into_utf8()?; + error!("{obj_name} property {i}: {}", name.as_str()?) + } + Ok(()) +} + +#[module_exports] +fn init(exports: JsObject, env: Env) -> napi_ohos::Result<()> { + initialize_logging_once(); + info!("simpleservo init function called"); + if let Ok(xcomponent) = exports.get_named_property::<JsObject>("__NATIVE_XCOMPONENT_OBJ__") { + register_xcomponent_callbacks(&env, &xcomponent)?; + } + + info!("Finished init"); + Ok(()) +} + +#[napi(js_name = "loadURL")] +pub fn load_url(url: String) -> Undefined { + debug!("load url"); + call(ServoAction::LoadUrl(url)).expect("Failed to load url"); +} + +#[napi(js_name = "registerURLcallback")] +pub fn register_url_callback(cb: JsFunction) -> napi_ohos::Result<()> { + info!("register_url_callback called!"); + let tsfn: ThreadsafeFunction<String, ErrorStrategy::Fatal> = + cb.create_threadsafe_function(1, |ctx| { + debug!( + "url callback argument transformer called with arg {}", + ctx.value + ); + let s = ctx + .env + .create_string_from_std(ctx.value) + .inspect_err(|e| error!("Failed to create JsString: {e:?}"))?; + Ok(vec![s]) + })?; + // We ignore any error for now - but probably we should propagate it back to the TS layer. + let _ = SET_URL_BAR_CB + .set(tsfn) + .inspect_err(|_| warn!("Failed to set URL callback - register_url_callback called twice?")); + Ok(()) +} + +#[napi] +pub fn init_servo(init_opts: InitOpts) -> napi_ohos::Result<()> { + info!("Servo is being initialised with the following Options: "); + info!( + "Device Type: {}, DisplayDensity: {}", + init_opts.device_type, init_opts.display_density + ); + 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"); + Ok(()) +} + +#[derive(Clone)] +pub struct WakeupCallback { + chan: Sender<ServoAction>, +} + +impl WakeupCallback { + pub(crate) fn new(chan: Sender<ServoAction>) -> Self { + WakeupCallback { chan } + } +} + +impl EventLoopWaker for WakeupCallback { + fn clone_box(&self) -> Box<dyn EventLoopWaker> { + Box::new(self.clone()) + } + + fn wake(&self) { + info!("wake called!"); + self.chan.send(ServoAction::WakeUp).unwrap_or_else(|e| { + error!("Failed to send wake message with: {e}"); + }); + } +} + +struct HostCallbacks {} + +impl HostCallbacks { + pub fn new() -> Self { + HostCallbacks {} + } +} + +#[allow(unused)] +impl HostTrait for HostCallbacks { + fn prompt_alert(&self, msg: String, trusted: bool) { + warn!("prompt_alert not implemented. Cancelled. {}", msg); + } + + fn prompt_yes_no(&self, msg: String, trusted: bool) -> PromptResult { + warn!("Prompt not implemented. Cancelled. {}", msg); + PromptResult::Secondary + } + + fn prompt_ok_cancel(&self, msg: String, trusted: bool) -> PromptResult { + warn!("Prompt not implemented. Cancelled. {}", msg); + PromptResult::Secondary + } + + fn prompt_input(&self, msg: String, default: String, trusted: bool) -> Option<String> { + warn!("Input prompt not implemented. Cancelled. {}", msg); + Some(default) + } + + fn show_context_menu(&self, title: Option<String>, items: Vec<String>) { + warn!("show_context_menu not implemented") + } + + fn on_load_started(&self) { + warn!("on_load_started not implemented") + } + + fn on_load_ended(&self) { + warn!("on_load_ended not implemented") + } + + fn on_title_changed(&self, title: Option<String>) { + warn!("on_title_changed not implemented") + } + + fn on_allow_navigation(&self, url: String) -> bool { + true + } + + fn on_url_changed(&self, url: String) { + debug!("Hosttrait `on_url_changed` called with new url: {url}"); + if let Some(cb) = SET_URL_BAR_CB.get() { + cb.call(url, ThreadsafeFunctionCallMode::Blocking); + } else { + warn!("`on_url_changed` called without a registered callback") + } + } + + fn on_history_changed(&self, can_go_back: bool, can_go_forward: bool) {} + + fn on_animating_changed(&self, animating: bool) {} + + fn on_shutdown_complete(&self) {} + + fn on_ime_show( + &self, + input_type: servo::embedder_traits::InputMethodType, + text: Option<(String, i32)>, + multiline: bool, + bounds: servo::webrender_api::units::DeviceIntRect, + ) { + warn!("on_title_changed not implemented") + } + + fn on_ime_hide(&self) { + warn!("on_title_changed not implemented") + } + + fn get_clipboard_contents(&self) -> Option<String> { + warn!("get_clipboard_contents not implemented"); + None + } + + fn set_clipboard_contents(&self, contents: String) { + warn!("set_clipboard_contents not implemented"); + } + + fn on_media_session_metadata(&self, title: String, artist: String, album: String) { + warn!("on_media_session_metadata not implemented"); + } + + fn on_media_session_playback_state_change( + &self, + state: servo::embedder_traits::MediaSessionPlaybackState, + ) { + warn!("on_media_session_playback_state_change not implemented"); + } + + fn on_media_session_set_position_state( + &self, + duration: f64, + position: f64, + playback_rate: f64, + ) { + warn!("on_media_session_set_position_state not implemented"); + } + + fn on_devtools_started(&self, port: Result<u16, ()>, token: String) { + warn!("on_devtools_started not implemented"); + } + + fn on_panic(&self, reason: String, backtrace: Option<String>) { + error!("Panic: {reason},"); + if let Some(bt) = backtrace { + error!("Backtrace: {bt:?}") + } + } +} diff --git a/ports/servoshell/egl/ohos/simpleservo.rs b/ports/servoshell/egl/ohos/simpleservo.rs new file mode 100644 index 00000000000..4bda080a693 --- /dev/null +++ b/ports/servoshell/egl/ohos/simpleservo.rs @@ -0,0 +1,110 @@ +/* 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::cell::RefCell; +use std::convert::TryInto; +use std::os::raw::c_void; +use std::path::PathBuf; +use std::rc::Rc; + +use log::{debug, error, info}; +use ohos_sys::ace::xcomponent::native_interface_xcomponent::{ + OH_NativeXComponent, OH_NativeXComponent_GetXComponentSize, +}; +use servo::compositing::windowing::EmbedderEvent; +use servo::compositing::CompositeTarget; +use servo::embedder_traits::resources; +/// The EventLoopWaker::wake function will be called from any thread. +/// It will be called to notify embedder that some events are available, +/// and that perform_updates need to be called +pub use servo::embedder_traits::EventLoopWaker; +use servo::euclid::Size2D; +use servo::servo_url::ServoUrl; +use servo::webrender_traits::RenderingContext; +use servo::{self, gl, Servo}; +use surfman::{Connection, SurfaceType}; + +use crate::egl::host_trait::HostTrait; +use crate::egl::ohos::InitOpts; +use crate::egl::resources::ResourceReaderInstance; +use crate::egl::servo_glue::{ + Coordinates, ServoEmbedderCallbacks, ServoGlue, ServoWindowCallbacks, +}; + +/// Initialize Servo. At that point, we need a valid GL context. +/// In the future, this will be done in multiple steps. +pub fn init( + options: InitOpts, + native_window: *mut c_void, + xcomponent: *mut OH_NativeXComponent, + gl: Rc<dyn gl::Gl>, + waker: Box<dyn EventLoopWaker>, + callbacks: Box<dyn HostTrait>, +) -> Result<ServoGlue, &'static str> { + info!("Entered simpleservo init function"); + resources::set(Box::new(ResourceReaderInstance::new())); + + gl.clear_color(1.0, 1.0, 1.0, 1.0); + gl.clear(gl::COLOR_BUFFER_BIT); + gl.finish(); + + // Initialize surfman + let connection = Connection::new().or(Err("Failed to create connection"))?; + let adapter = connection + .create_adapter() + .or(Err("Failed to create adapter"))?; + + let mut width: u64 = 0; + let mut height: u64 = 0; + let res = unsafe { + OH_NativeXComponent_GetXComponentSize( + xcomponent, + native_window, + &mut width as *mut _, + &mut height as *mut _, + ) + }; + assert_eq!(res, 0, "OH_NativeXComponent_GetXComponentSize failed"); + let width: i32 = width.try_into().expect("Width too large"); + let height: i32 = height.try_into().expect("Height too large"); + + debug!("Creating surfman widget with width {width} and height {height}"); + let native_widget = unsafe { + connection.create_native_widget_from_ptr(native_window, Size2D::new(width, height)) + }; + let surface_type = SurfaceType::Widget { native_widget }; + + info!("Creating rendering context"); + let rendering_context = RenderingContext::create(&connection, &adapter, surface_type) + .or(Err("Failed to create surface manager"))?; + + info!("before ServoWindowCallbacks..."); + + let window_callbacks = Rc::new(ServoWindowCallbacks::new( + callbacks, + RefCell::new(Coordinates::new(0, 0, width, height, width, height)), + options.display_density as f32, + rendering_context.clone(), + )); + + let embedder_callbacks = Box::new(ServoEmbedderCallbacks::new(waker, None, gl.clone())); + + let servo = Servo::new( + embedder_callbacks, + window_callbacks.clone(), + // User agent: Mozilla/5.0 (<Phone|PC|Tablet>; HarmonyOS 5.0) bla bla + None, + CompositeTarget::Window, + ); + + let mut servo_glue = ServoGlue::new(rendering_context, servo.servo, window_callbacks); + + let initial_url = ServoUrl::parse(options.url.as_str()) + .inspect_err(|e| error!("Invalid initial Servo URL `{}`. Error: {e:?}", options.url)) + .ok() + .unwrap_or_else(|| ServoUrl::parse("about:blank").expect("Infallible")); + + let _ = servo_glue.process_event(EmbedderEvent::NewWebView(initial_url, servo.browser_id)); + + Ok(servo_glue) +} diff --git a/ports/servoshell/egl/resources.rs b/ports/servoshell/egl/resources.rs new file mode 100644 index 00000000000..9ada47afdb4 --- /dev/null +++ b/ports/servoshell/egl/resources.rs @@ -0,0 +1,56 @@ +/* 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::path::PathBuf; + +use servo::embedder_traits::resources::{Resource, ResourceReaderMethods}; + +pub(crate) struct ResourceReaderInstance; + +impl ResourceReaderInstance { + pub(crate) fn new() -> ResourceReaderInstance { + ResourceReaderInstance + } +} + +impl ResourceReaderMethods for ResourceReaderInstance { + fn read(&self, res: Resource) -> Vec<u8> { + Vec::from(match res { + Resource::Preferences => &include_bytes!("../../../resources/prefs.json")[..], + Resource::HstsPreloadList => { + &include_bytes!("../../../resources/hsts_preload.json")[..] + }, + Resource::BadCertHTML => &include_bytes!("../../../resources/badcert.html")[..], + Resource::NetErrorHTML => &include_bytes!("../../../resources/neterror.html")[..], + Resource::UserAgentCSS => &include_bytes!("../../../resources/user-agent.css")[..], + Resource::ServoCSS => &include_bytes!("../../../resources/servo.css")[..], + Resource::PresentationalHintsCSS => { + &include_bytes!("../../../resources/presentational-hints.css")[..] + }, + Resource::QuirksModeCSS => &include_bytes!("../../../resources/quirks-mode.css")[..], + Resource::RippyPNG => &include_bytes!("../../../resources/rippy.png")[..], + Resource::DomainList => &include_bytes!("../../../resources/public_domains.txt")[..], + Resource::BluetoothBlocklist => { + &include_bytes!("../../../resources/gatt_blocklist.txt")[..] + }, + Resource::MediaControlsCSS => { + &include_bytes!("../../../resources/media-controls.css")[..] + }, + Resource::MediaControlsJS => { + &include_bytes!("../../../resources/media-controls.js")[..] + }, + Resource::CrashHTML => &include_bytes!("../../../resources/crash.html")[..], + Resource::DirectoryListingHTML => { + &include_bytes!("../../../resources/directory-listing.html")[..] + }, + }) + } + + fn sandbox_access_files(&self) -> Vec<PathBuf> { + vec![] + } + + fn sandbox_access_files_dirs(&self) -> Vec<PathBuf> { + vec![] + } +} diff --git a/ports/servoshell/egl/servo_glue.rs b/ports/servoshell/egl/servo_glue.rs new file mode 100644 index 00000000000..882451a4196 --- /dev/null +++ b/ports/servoshell/egl/servo_glue.rs @@ -0,0 +1,701 @@ +/* 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::cell::RefCell; +use std::collections::HashMap; +use std::mem; +use std::os::raw::c_void; +use std::rc::Rc; + +use ipc_channel::ipc::IpcSender; +use log::{debug, info, warn}; +use servo::base::id::WebViewId; +use servo::compositing::windowing::{ + AnimationState, EmbedderCoordinates, EmbedderEvent, EmbedderMethods, MouseWindowEvent, + WindowMethods, +}; +use servo::embedder_traits::{ + ContextMenuResult, EmbedderMsg, EmbedderProxy, EventLoopWaker, MediaSessionEvent, + PermissionPrompt, PermissionRequest, PromptDefinition, PromptOrigin, PromptResult, +}; +use servo::euclid::{Point2D, Rect, Scale, Size2D, Vector2D}; +use servo::keyboard_types::{Key, KeyState, KeyboardEvent}; +use servo::script_traits::{ + MediaSessionActionType, MouseButton, TouchEventType, TouchId, TraversalDirection, +}; +use servo::style_traits::DevicePixel; +use servo::webrender_api::ScrollLocation; +use servo::webrender_traits::RenderingContext; +use servo::{gl, Servo, TopLevelBrowsingContextId}; + +use crate::egl::host_trait::HostTrait; + +#[derive(Clone, Debug)] +pub struct Coordinates { + pub viewport: Rect<i32, DevicePixel>, + pub framebuffer: Size2D<i32, DevicePixel>, +} + +impl Coordinates { + pub fn new( + x: i32, + y: i32, + width: i32, + height: i32, + fb_width: i32, + fb_height: i32, + ) -> Coordinates { + Coordinates { + viewport: Rect::new(Point2D::new(x, y), Size2D::new(width, height)), + framebuffer: Size2D::new(fb_width, fb_height), + } + } +} + +pub(super) struct ServoWindowCallbacks { + host_callbacks: Box<dyn HostTrait>, + coordinates: RefCell<Coordinates>, + density: f32, + rendering_context: RenderingContext, +} + +impl ServoWindowCallbacks { + pub(super) fn new( + host_callbacks: Box<dyn HostTrait>, + coordinates: RefCell<Coordinates>, + density: f32, + rendering_context: RenderingContext, + ) -> Self { + Self { + host_callbacks, + coordinates, + density, + rendering_context, + } + } +} + +#[derive(Debug)] +pub struct WebView {} + +pub struct ServoGlue { + rendering_context: RenderingContext, + servo: Servo<ServoWindowCallbacks>, + batch_mode: bool, + callbacks: Rc<ServoWindowCallbacks>, + events: Vec<EmbedderEvent>, + context_menu_sender: Option<IpcSender<ContextMenuResult>>, + + /// List of top-level browsing contexts. + /// Modified by EmbedderMsg::WebViewOpened and EmbedderMsg::WebViewClosed, + /// and we exit if it ever becomes empty. + webviews: HashMap<WebViewId, WebView>, + + /// The order in which the webviews were created. + creation_order: Vec<WebViewId>, + + /// The webview that is currently focused. + /// Modified by EmbedderMsg::WebViewFocused and EmbedderMsg::WebViewBlurred. + focused_webview_id: Option<WebViewId>, +} + +#[allow(unused)] +impl ServoGlue { + pub(super) fn new( + rendering_context: RenderingContext, + servo: Servo<ServoWindowCallbacks>, + callbacks: Rc<ServoWindowCallbacks>, + ) -> Self { + Self { + rendering_context, + servo, + batch_mode: false, + callbacks, + events: vec![], + context_menu_sender: None, + webviews: HashMap::default(), + creation_order: vec![], + focused_webview_id: None, + } + } + + fn get_browser_id(&self) -> Result<TopLevelBrowsingContextId, &'static str> { + let webview_id = match self.focused_webview_id { + Some(id) => id, + None => return Err("No focused WebViewId yet."), + }; + Ok(webview_id) + } + + /// Request shutdown. Will call on_shutdown_complete. + pub fn request_shutdown(&mut self) -> Result<(), &'static str> { + self.process_event(EmbedderEvent::Quit) + } + + /// Call after on_shutdown_complete + pub fn deinit(self) { + self.servo.deinit(); + } + + /// Returns the webrender surface management integration interface. + /// This provides the embedder access to the current front buffer. + pub fn surfman(&self) -> RenderingContext { + self.rendering_context.clone() + } + + /// This is the Servo heartbeat. This needs to be called + /// everytime wakeup is called or when embedder wants Servo + /// to act on its pending events. + pub fn perform_updates(&mut self) -> Result<(), &'static str> { + debug!("perform_updates"); + let events = mem::replace(&mut self.events, Vec::new()); + self.servo.handle_events(events); + let r = self.handle_servo_events(); + debug!("done perform_updates"); + r + } + + /// In batch mode, Servo won't call perform_updates automatically. + /// This can be useful when the embedder wants to control when Servo + /// acts on its pending events. For example, if the embedder wants Servo + /// to act on the scroll events only at a certain time, not everytime + /// scroll() is called. + pub fn set_batch_mode(&mut self, batch: bool) -> Result<(), &'static str> { + debug!("set_batch_mode"); + self.batch_mode = batch; + Ok(()) + } + + /// Load an URL. + pub fn load_uri(&mut self, url: &str) -> Result<(), &'static str> { + info!("load_uri: {}", url); + crate::parser::location_bar_input_to_url(url) + .ok_or("Can't parse URL") + .and_then(|url| { + let browser_id = self.get_browser_id()?; + let event = EmbedderEvent::LoadUrl(browser_id, url); + self.process_event(event) + }) + } + + /// Reload the page. + pub fn clear_cache(&mut self) -> Result<(), &'static str> { + info!("clear_cache"); + let event = EmbedderEvent::ClearCache; + self.process_event(event) + } + + /// Reload the page. + pub fn reload(&mut self) -> Result<(), &'static str> { + info!("reload"); + let browser_id = self.get_browser_id()?; + let event = EmbedderEvent::Reload(browser_id); + self.process_event(event) + } + + /// Redraw the page. + pub fn refresh(&mut self) -> Result<(), &'static str> { + info!("refresh"); + self.process_event(EmbedderEvent::Refresh) + } + + /// Stop loading the page. + pub fn stop(&mut self) -> Result<(), &'static str> { + warn!("TODO can't stop won't stop"); + Ok(()) + } + + /// Go back in history. + pub fn go_back(&mut self) -> Result<(), &'static str> { + info!("go_back"); + let browser_id = self.get_browser_id()?; + let event = EmbedderEvent::Navigation(browser_id, TraversalDirection::Back(1)); + self.process_event(event) + } + + /// Go forward in history. + pub fn go_forward(&mut self) -> Result<(), &'static str> { + info!("go_forward"); + let browser_id = self.get_browser_id()?; + let event = EmbedderEvent::Navigation(browser_id, TraversalDirection::Forward(1)); + self.process_event(event) + } + + /// Let Servo know that the window has been resized. + pub fn resize(&mut self, coordinates: Coordinates) -> Result<(), &'static str> { + info!("resize"); + *self.callbacks.coordinates.borrow_mut() = coordinates; + self.process_event(EmbedderEvent::WindowResize) + } + + /// Start scrolling. + /// x/y are scroll coordinates. + /// dx/dy are scroll deltas. + #[cfg(not(target_env = "ohos"))] + pub fn scroll_start(&mut self, dx: f32, dy: f32, x: i32, y: i32) -> Result<(), &'static str> { + let delta = Vector2D::new(dx, dy); + let scroll_location = ScrollLocation::Delta(delta); + let event = + EmbedderEvent::Scroll(scroll_location, Point2D::new(x, y), TouchEventType::Down); + self.process_event(event) + } + + /// Scroll. + /// x/y are scroll coordinates. + /// dx/dy are scroll deltas. + pub fn scroll(&mut self, dx: f32, dy: f32, x: i32, y: i32) -> Result<(), &'static str> { + let delta = Vector2D::new(dx, dy); + let scroll_location = ScrollLocation::Delta(delta); + let event = + EmbedderEvent::Scroll(scroll_location, Point2D::new(x, y), TouchEventType::Move); + self.process_event(event) + } + + /// End scrolling. + /// x/y are scroll coordinates. + /// dx/dy are scroll deltas. + #[cfg(not(target_env = "ohos"))] + pub fn scroll_end(&mut self, dx: f32, dy: f32, x: i32, y: i32) -> Result<(), &'static str> { + let delta = Vector2D::new(dx, dy); + let scroll_location = ScrollLocation::Delta(delta); + let event = EmbedderEvent::Scroll(scroll_location, Point2D::new(x, y), TouchEventType::Up); + self.process_event(event) + } + + /// Touch event: press down + pub fn touch_down(&mut self, x: f32, y: f32, pointer_id: i32) -> Result<(), &'static str> { + let event = EmbedderEvent::Touch( + TouchEventType::Down, + TouchId(pointer_id), + Point2D::new(x as f32, y as f32), + ); + self.process_event(event) + } + + /// Touch event: move touching finger + pub fn touch_move(&mut self, x: f32, y: f32, pointer_id: i32) -> Result<(), &'static str> { + let event = EmbedderEvent::Touch( + TouchEventType::Move, + TouchId(pointer_id), + Point2D::new(x as f32, y as f32), + ); + self.process_event(event) + } + + /// Touch event: Lift touching finger + pub fn touch_up(&mut self, x: f32, y: f32, pointer_id: i32) -> Result<(), &'static str> { + let event = EmbedderEvent::Touch( + TouchEventType::Up, + TouchId(pointer_id), + Point2D::new(x as f32, y as f32), + ); + self.process_event(event) + } + + /// Cancel touch event + pub fn touch_cancel(&mut self, x: f32, y: f32, pointer_id: i32) -> Result<(), &'static str> { + let event = EmbedderEvent::Touch( + TouchEventType::Cancel, + TouchId(pointer_id), + Point2D::new(x as f32, y as f32), + ); + self.process_event(event) + } + + /// Register a mouse movement. + pub fn mouse_move(&mut self, x: f32, y: f32) -> Result<(), &'static str> { + let point = Point2D::new(x, y); + let event = EmbedderEvent::MouseWindowMoveEventClass(point); + self.process_event(event) + } + + /// Register a mouse button press. + pub fn mouse_down(&mut self, x: f32, y: f32, button: MouseButton) -> Result<(), &'static str> { + let point = Point2D::new(x, y); + let event = + EmbedderEvent::MouseWindowEventClass(MouseWindowEvent::MouseDown(button, point)); + self.process_event(event) + } + + /// Register a mouse button release. + pub fn mouse_up(&mut self, x: f32, y: f32, button: MouseButton) -> Result<(), &'static str> { + let point = Point2D::new(x, y); + let event = EmbedderEvent::MouseWindowEventClass(MouseWindowEvent::MouseUp(button, point)); + self.process_event(event) + } + + /// Start pinchzoom. + /// x/y are pinch origin coordinates. + pub fn pinchzoom_start(&mut self, factor: f32, _x: u32, _y: u32) -> Result<(), &'static str> { + self.process_event(EmbedderEvent::PinchZoom(factor)) + } + + /// Pinchzoom. + /// x/y are pinch origin coordinates. + pub fn pinchzoom(&mut self, factor: f32, _x: u32, _y: u32) -> Result<(), &'static str> { + self.process_event(EmbedderEvent::PinchZoom(factor)) + } + + /// End pinchzoom. + /// x/y are pinch origin coordinates. + pub fn pinchzoom_end(&mut self, factor: f32, _x: u32, _y: u32) -> Result<(), &'static str> { + self.process_event(EmbedderEvent::PinchZoom(factor)) + } + + /// Perform a click. + pub fn click(&mut self, x: f32, y: f32) -> Result<(), &'static str> { + let mouse_event = MouseWindowEvent::Click(MouseButton::Left, Point2D::new(x, y)); + let event = EmbedderEvent::MouseWindowEventClass(mouse_event); + self.process_event(event) + } + + pub fn key_down(&mut self, key: Key) -> Result<(), &'static str> { + let key_event = KeyboardEvent { + state: KeyState::Down, + key, + ..KeyboardEvent::default() + }; + self.process_event(EmbedderEvent::Keyboard(key_event)) + } + + pub fn key_up(&mut self, key: Key) -> Result<(), &'static str> { + let key_event = KeyboardEvent { + state: KeyState::Up, + key, + ..KeyboardEvent::default() + }; + self.process_event(EmbedderEvent::Keyboard(key_event)) + } + + pub fn pause_compositor(&mut self) -> Result<(), &'static str> { + self.process_event(EmbedderEvent::InvalidateNativeSurface) + } + + pub fn resume_compositor( + &mut self, + native_surface: *mut c_void, + coords: Coordinates, + ) -> Result<(), &'static str> { + if native_surface.is_null() { + panic!("null passed for native_surface"); + } + self.process_event(EmbedderEvent::ReplaceNativeSurface( + native_surface, + coords.framebuffer, + )) + } + + pub fn media_session_action( + &mut self, + action: MediaSessionActionType, + ) -> Result<(), &'static str> { + info!("Media session action {:?}", action); + self.process_event(EmbedderEvent::MediaSessionAction(action)) + } + + pub fn set_throttled(&mut self, throttled: bool) -> Result<(), &'static str> { + info!("set_throttled"); + if let Ok(id) = self.get_browser_id() { + let event = EmbedderEvent::SetWebViewThrottled(id, throttled); + self.process_event(event) + } else { + // Ignore visibility change if no browser has been created yet. + Ok(()) + } + } + + pub fn ime_dismissed(&mut self) -> Result<(), &'static str> { + info!("ime_dismissed"); + self.process_event(EmbedderEvent::IMEDismissed) + } + + pub fn on_context_menu_closed( + &mut self, + result: ContextMenuResult, + ) -> Result<(), &'static str> { + if let Some(sender) = self.context_menu_sender.take() { + let _ = sender.send(result); + } else { + warn!("Trying to close a context menu when no context menu is active"); + } + Ok(()) + } + + pub(super) fn process_event(&mut self, event: EmbedderEvent) -> Result<(), &'static str> { + self.events.push(event); + if !self.batch_mode { + self.perform_updates() + } else { + Ok(()) + } + } + + fn handle_servo_events(&mut self) -> Result<(), &'static str> { + let mut need_update = false; + let mut need_present = false; + for (browser_id, event) in self.servo.get_events() { + match event { + EmbedderMsg::ChangePageTitle(title) => { + self.callbacks.host_callbacks.on_title_changed(title); + }, + EmbedderMsg::AllowNavigationRequest(pipeline_id, url) => { + if let Some(_browser_id) = browser_id { + let data: bool = self + .callbacks + .host_callbacks + .on_allow_navigation(url.to_string()); + let window_event = + EmbedderEvent::AllowNavigationResponse(pipeline_id, data); + self.events.push(window_event); + need_update = true; + } + }, + EmbedderMsg::HistoryChanged(entries, current) => { + let can_go_back = current > 0; + let can_go_forward = current < entries.len() - 1; + self.callbacks + .host_callbacks + .on_history_changed(can_go_back, can_go_forward); + self.callbacks + .host_callbacks + .on_url_changed(entries[current].clone().to_string()); + }, + EmbedderMsg::LoadStart => { + self.callbacks.host_callbacks.on_load_started(); + }, + EmbedderMsg::LoadComplete => { + self.callbacks.host_callbacks.on_load_ended(); + }, + EmbedderMsg::GetSelectedBluetoothDevice(_, sender) => { + let _ = sender.send(None); + }, + EmbedderMsg::AllowUnload(sender) => { + let _ = sender.send(true); + }, + EmbedderMsg::ShowContextMenu(sender, title, items) => { + if self.context_menu_sender.is_some() { + warn!( + "Trying to show a context menu when a context menu is already active" + ); + let _ = sender.send(ContextMenuResult::Ignored); + } else { + self.context_menu_sender = Some(sender); + self.callbacks + .host_callbacks + .show_context_menu(title, items); + } + }, + EmbedderMsg::Prompt(definition, origin) => { + let cb = &self.callbacks.host_callbacks; + let trusted = origin == PromptOrigin::Trusted; + let res = match definition { + PromptDefinition::Alert(message, sender) => { + sender.send(cb.prompt_alert(message, trusted)) + }, + PromptDefinition::OkCancel(message, sender) => { + sender.send(cb.prompt_ok_cancel(message, trusted)) + }, + PromptDefinition::YesNo(message, sender) => { + sender.send(cb.prompt_yes_no(message, trusted)) + }, + PromptDefinition::Input(message, default, sender) => { + sender.send(cb.prompt_input(message, default, trusted)) + }, + }; + if let Err(e) = res { + let reason = format!("Failed to send Prompt response: {}", e); + self.events + .push(EmbedderEvent::SendError(browser_id, reason)); + } + }, + EmbedderMsg::AllowOpeningWebView(response_chan) => { + // Note: would be a place to handle pop-ups config. + // see Step 7 of #the-rules-for-choosing-a-browsing-context-given-a-browsing-context-name + if let Err(e) = response_chan.send(true) { + warn!("Failed to send AllowOpeningBrowser response: {}", e); + }; + }, + EmbedderMsg::WebViewOpened(new_webview_id) => { + self.webviews.insert(new_webview_id, WebView {}); + self.creation_order.push(new_webview_id); + self.events + .push(EmbedderEvent::FocusWebView(new_webview_id)); + }, + EmbedderMsg::WebViewClosed(webview_id) => { + self.webviews.retain(|&id, _| id != webview_id); + self.creation_order.retain(|&id| id != webview_id); + self.focused_webview_id = None; + if let Some(&newest_webview_id) = self.creation_order.last() { + self.events + .push(EmbedderEvent::FocusWebView(newest_webview_id)); + } else { + self.events.push(EmbedderEvent::Quit); + } + }, + EmbedderMsg::WebViewFocused(webview_id) => { + self.focused_webview_id = Some(webview_id); + }, + EmbedderMsg::WebViewBlurred => { + self.focused_webview_id = None; + }, + EmbedderMsg::GetClipboardContents(sender) => { + let contents = self.callbacks.host_callbacks.get_clipboard_contents(); + let _ = sender.send(contents.unwrap_or("".to_owned())); + }, + EmbedderMsg::SetClipboardContents(text) => { + self.callbacks.host_callbacks.set_clipboard_contents(text); + }, + EmbedderMsg::Shutdown => { + self.callbacks.host_callbacks.on_shutdown_complete(); + }, + EmbedderMsg::PromptPermission(prompt, sender) => { + let message = match prompt { + PermissionPrompt::Request(permission_name) => { + format!("Do you want to grant permission for {:?}?", permission_name) + }, + PermissionPrompt::Insecure(permission_name) => { + format!( + "The {:?} feature is only safe to use in secure context, but servo can't guarantee\n\ + that the current context is secure. Do you want to proceed and grant permission?", + permission_name + ) + }, + }; + + let result = match self.callbacks.host_callbacks.prompt_yes_no(message, true) { + PromptResult::Primary => PermissionRequest::Granted, + PromptResult::Secondary | PromptResult::Dismissed => { + PermissionRequest::Denied + }, + }; + + let _ = sender.send(result); + }, + EmbedderMsg::ShowIME(kind, text, multiline, bounds) => { + self.callbacks + .host_callbacks + .on_ime_show(kind, text, multiline, bounds); + }, + EmbedderMsg::HideIME => { + self.callbacks.host_callbacks.on_ime_hide(); + }, + EmbedderMsg::MediaSessionEvent(event) => { + match event { + MediaSessionEvent::SetMetadata(metadata) => { + self.callbacks.host_callbacks.on_media_session_metadata( + metadata.title, + metadata.artist, + metadata.album, + ) + }, + MediaSessionEvent::PlaybackStateChange(state) => self + .callbacks + .host_callbacks + .on_media_session_playback_state_change(state), + MediaSessionEvent::SetPositionState(position_state) => self + .callbacks + .host_callbacks + .on_media_session_set_position_state( + position_state.duration, + position_state.position, + position_state.playback_rate, + ), + }; + }, + EmbedderMsg::OnDevtoolsStarted(port, token) => { + self.callbacks + .host_callbacks + .on_devtools_started(port, token); + }, + EmbedderMsg::Panic(reason, backtrace) => { + self.callbacks.host_callbacks.on_panic(reason, backtrace); + }, + EmbedderMsg::ReadyToPresent(_webview_ids) => { + need_present = true; + }, + EmbedderMsg::Status(..) | + EmbedderMsg::SelectFiles(..) | + EmbedderMsg::MoveTo(..) | + EmbedderMsg::ResizeTo(..) | + EmbedderMsg::Keyboard(..) | + EmbedderMsg::SetCursor(..) | + EmbedderMsg::NewFavicon(..) | + EmbedderMsg::HeadParsed | + EmbedderMsg::SetFullscreenState(..) | + EmbedderMsg::ReportProfile(..) | + EmbedderMsg::EventDelivered(..) => {}, + } + } + + if need_update { + let _ = self.perform_updates(); + } + if need_present { + self.servo.present(); + } + Ok(()) + } +} + +pub(super) struct ServoEmbedderCallbacks { + waker: Box<dyn EventLoopWaker>, + xr_discovery: Option<webxr::Discovery>, + #[allow(unused)] + gl: Rc<dyn gl::Gl>, +} + +impl ServoEmbedderCallbacks { + pub(super) fn new( + waker: Box<dyn EventLoopWaker>, + xr_discovery: Option<webxr::Discovery>, + gl: Rc<dyn gl::Gl>, + ) -> Self { + Self { + waker, + xr_discovery, + gl, + } + } +} + +impl EmbedderMethods for ServoEmbedderCallbacks { + fn create_event_loop_waker(&mut self) -> Box<dyn EventLoopWaker> { + debug!("EmbedderMethods::create_event_loop_waker"); + self.waker.clone() + } + + fn register_webxr( + &mut self, + registry: &mut webxr::MainThreadRegistry, + _embedder_proxy: EmbedderProxy, + ) { + debug!("EmbedderMethods::register_xr"); + if let Some(discovery) = self.xr_discovery.take() { + registry.register(discovery); + } + } +} + +impl WindowMethods for ServoWindowCallbacks { + fn get_coordinates(&self) -> EmbedderCoordinates { + let coords = self.coordinates.borrow(); + EmbedderCoordinates { + viewport: coords.viewport.to_box2d(), + framebuffer: coords.framebuffer, + window: (coords.viewport.size, Point2D::new(0, 0)), + screen: coords.viewport.size, + screen_avail: coords.viewport.size, + hidpi_factor: Scale::new(self.density), + } + } + + fn set_animation_state(&self, state: AnimationState) { + debug!("WindowMethods::set_animation_state: {:?}", state); + self.host_callbacks + .on_animating_changed(state == AnimationState::Animating); + } + + fn rendering_context(&self) -> RenderingContext { + self.rendering_context.clone() + } +} diff --git a/ports/servoshell/lib.rs b/ports/servoshell/lib.rs index 82e30d28467..8789f1a67c7 100644 --- a/ports/servoshell/lib.rs +++ b/ports/servoshell/lib.rs @@ -14,14 +14,15 @@ mod backtrace; mod crash_handler; #[cfg(not(any(target_os = "android", target_env = "ohos")))] pub(crate) mod desktop; +#[cfg(any(target_os = "android", target_env = "ohos"))] +mod egl; #[cfg(not(target_os = "android"))] mod panic_hook; mod parser; mod prefs; +#[cfg(not(any(target_os = "android", target_env = "ohos")))] mod resources; -mod egl; - pub mod platform { #[cfg(target_os = "macos")] pub use crate::platform::macos::deinit; diff --git a/ports/servoshell/main.rs b/ports/servoshell/main.rs index 84d878eea71..08fef65b91f 100644 --- a/ports/servoshell/main.rs +++ b/ports/servoshell/main.rs @@ -22,12 +22,15 @@ fn main() { cfg_if::cfg_if! { - if #[cfg(not(target_os = "android"))] { + if #[cfg(not(any(target_os = "android", target_env = "ohos")))] { servoshell::main() } else { println!( - "Cannot start /ports/servoshell/ on Android. \ - Use /support/android/apk/ + /ports/jniapi/ instead" + "Cannot run the servoshell `bin` executable on platforms such as \ + Android or OpenHarmony. On these platforms you need to compile \ + the servoshell library as a `cdylib` and integrate it with the \ + platform app code into an `apk` (android) or `hap` (OpenHarmony).\ + For Android `mach build` will do these steps automatically for you." ); } } |