/* 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::os::raw::c_void; use std::rc::Rc; use ipc_channel::ipc::IpcSender; use keyboard_types::{CompositionEvent, CompositionState}; use log::{debug, error, info, warn}; use servo::base::id::WebViewId; use servo::compositing::windowing::{ AnimationState, EmbedderCoordinates, EmbedderMethods, MouseWindowEvent, WindowMethods, }; use servo::euclid::{Box2D, Point2D, Rect, Scale, Size2D, Vector2D}; use servo::servo_geometry::DeviceIndependentPixel; use servo::webrender_api::units::{DevicePixel, DeviceRect}; use servo::webrender_api::ScrollLocation; use servo::webrender_traits::SurfmanRenderingContext; use servo::{ ContextMenuResult, EmbedderMsg, EmbedderProxy, EventLoopWaker, Key, KeyState, KeyboardEvent, MediaSessionActionType, MediaSessionEvent, MouseButton, PermissionPrompt, PermissionRequest, PromptDefinition, PromptOrigin, PromptResult, Servo, TouchEventType, TouchId, WebView, }; use url::Url; use crate::egl::host_trait::HostTrait; use crate::prefs::ServoShellPreferences; #[derive(Clone, Debug)] pub struct Coordinates { pub viewport: Rect, pub framebuffer: Size2D, } 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, coordinates: RefCell, hidpi_factor: Scale, } impl ServoWindowCallbacks { pub(super) fn new( host_callbacks: Box, coordinates: RefCell, hidpi_factor: f32, ) -> Self { Self { host_callbacks, coordinates, hidpi_factor: Scale::new(hidpi_factor), } } } pub struct ServoGlue { rendering_context: SurfmanRenderingContext, servo: Servo, need_present: bool, callbacks: Rc, context_menu_sender: Option>, /// List of top-level browsing contexts. /// Modified by EmbedderMsg::WebViewOpened and EmbedderMsg::WebViewClosed, /// and we exit if it ever becomes empty. webviews: HashMap, /// The order in which the webviews were created. creation_order: Vec, /// The webview that is currently focused. /// Modified by EmbedderMsg::WebViewFocused and EmbedderMsg::WebViewBlurred. focused_webview_id: Option, /// servoshell specific preferences created during startup of the application. servoshell_preferences: ServoShellPreferences, } #[allow(unused)] impl ServoGlue { pub(super) fn new( initial_url: Option, rendering_context: SurfmanRenderingContext, servo: Servo, callbacks: Rc, servoshell_preferences: ServoShellPreferences, ) -> Self { let initial_url = initial_url.and_then(|string| Url::parse(&string).ok()); let initial_url = initial_url .or_else(|| Url::parse(&servoshell_preferences.homepage).ok()) .or_else(|| Url::parse("about:blank").ok()) .unwrap(); let webview = servo.new_webview(initial_url); let webview_id = webview.id(); let webviews = [(webview_id, webview)].into(); Self { rendering_context, servo, need_present: false, callbacks, context_menu_sender: None, webviews, creation_order: vec![], focused_webview_id: Some(webview_id), servoshell_preferences, } } fn get_browser_id(&self) -> Result { let webview_id = match self.focused_webview_id { Some(id) => id, None => return Err("No focused WebViewId yet."), }; Ok(webview_id) } fn newest_webview(&self) -> Option<&WebView> { self.creation_order .last() .and_then(|id| self.webviews.get(id)) } fn active_webview(&self) -> &WebView { self.focused_webview_id .and_then(|id| self.webviews.get(&id)) .or(self.newest_webview()) .expect("Should always have an active WebView") } /// Request shutdown. Will call on_shutdown_complete. pub fn request_shutdown(&mut self) { self.servo.start_shutting_down(); self.maybe_perform_updates(); } /// 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) -> SurfmanRenderingContext { 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) { debug!("perform_updates"); self.servo.handle_events(vec![]); let _ = self.handle_servo_events(); debug!("done perform_updates"); } /// Load an URL. pub fn load_uri(&mut self, url: &str) { info!("load_uri: {}", url); let Some(url) = crate::parser::location_bar_input_to_url(url, &self.servoshell_preferences.searchpage) else { warn!("Cannot parse URL"); return; }; self.active_webview().load(url.into_url()); } /// Reload the page. pub fn reload(&mut self) { info!("reload"); self.active_webview().reload(); self.maybe_perform_updates() } /// Redraw the page. pub fn refresh(&mut self) { info!("refresh"); self.active_webview().composite(); self.maybe_perform_updates() } /// Stop loading the page. pub fn stop(&mut self) { warn!("TODO can't stop won't stop"); } /// Go back in history. pub fn go_back(&mut self) { info!("go_back"); self.active_webview().go_back(1); self.maybe_perform_updates() } /// Go forward in history. pub fn go_forward(&mut self) { info!("go_forward"); self.active_webview().go_forward(1); self.maybe_perform_updates() } /// Let Servo know that the window has been resized. pub fn resize(&mut self, coordinates: Coordinates) { info!("resize to {:?}", coordinates); let size = coordinates.viewport.size; let _ = self .rendering_context .resize(Size2D::new(size.width, size.height)) .inspect_err(|e| error!("Failed to resize rendering context: {e:?}")); *self.callbacks.coordinates.borrow_mut() = coordinates; self.active_webview().notify_rendering_context_resized(); self.active_webview() .move_resize(DeviceRect::from_size(size.to_f32())); self.maybe_perform_updates() } /// 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) { let delta = Vector2D::new(dx, dy); let scroll_location = ScrollLocation::Delta(delta); self.active_webview().notify_scroll_event( scroll_location, Point2D::new(x, y), TouchEventType::Down, ); self.maybe_perform_updates() } /// Scroll. /// x/y are scroll coordinates. /// dx/dy are scroll deltas. pub fn scroll(&mut self, dx: f32, dy: f32, x: i32, y: i32) { let delta = Vector2D::new(dx, dy); let scroll_location = ScrollLocation::Delta(delta); self.active_webview().notify_scroll_event( scroll_location, Point2D::new(x, y), TouchEventType::Move, ); self.maybe_perform_updates() } /// 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) { let delta = Vector2D::new(dx, dy); let scroll_location = ScrollLocation::Delta(delta); self.active_webview().notify_scroll_event( scroll_location, Point2D::new(x, y), TouchEventType::Up, ); self.maybe_perform_updates() } /// Touch event: press down pub fn touch_down(&mut self, x: f32, y: f32, pointer_id: i32) { self.active_webview().notify_touch_event( TouchEventType::Down, TouchId(pointer_id), Point2D::new(x, y), ); self.maybe_perform_updates() } /// Touch event: move touching finger pub fn touch_move(&mut self, x: f32, y: f32, pointer_id: i32) { self.active_webview().notify_touch_event( TouchEventType::Move, TouchId(pointer_id), Point2D::new(x, y), ); self.maybe_perform_updates() } /// Touch event: Lift touching finger pub fn touch_up(&mut self, x: f32, y: f32, pointer_id: i32) { self.active_webview().notify_touch_event( TouchEventType::Up, TouchId(pointer_id), Point2D::new(x, y), ); self.maybe_perform_updates() } /// Cancel touch event pub fn touch_cancel(&mut self, x: f32, y: f32, pointer_id: i32) { self.active_webview().notify_touch_event( TouchEventType::Cancel, TouchId(pointer_id), Point2D::new(x, y), ); self.maybe_perform_updates() } /// Register a mouse movement. pub fn mouse_move(&mut self, x: f32, y: f32) { self.active_webview() .notify_pointer_move_event(Point2D::new(x, y)); self.maybe_perform_updates() } /// Register a mouse button press. pub fn mouse_down(&mut self, x: f32, y: f32, button: MouseButton) { self.active_webview() .notify_pointer_button_event(MouseWindowEvent::MouseDown(button, Point2D::new(x, y))); self.maybe_perform_updates() } /// Register a mouse button release. pub fn mouse_up(&mut self, x: f32, y: f32, button: MouseButton) { self.active_webview() .notify_pointer_button_event(MouseWindowEvent::MouseUp(button, Point2D::new(x, y))); self.maybe_perform_updates() } /// Start pinchzoom. /// x/y are pinch origin coordinates. pub fn pinchzoom_start(&mut self, factor: f32, _x: u32, _y: u32) { self.active_webview().set_pinch_zoom(factor); self.maybe_perform_updates() } /// Pinchzoom. /// x/y are pinch origin coordinates. pub fn pinchzoom(&mut self, factor: f32, _x: u32, _y: u32) { self.active_webview().set_pinch_zoom(factor); self.maybe_perform_updates() } /// End pinchzoom. /// x/y are pinch origin coordinates. pub fn pinchzoom_end(&mut self, factor: f32, _x: u32, _y: u32) { self.active_webview().set_pinch_zoom(factor); self.maybe_perform_updates() } /// Perform a click. pub fn click(&mut self, x: f32, y: f32) { self.active_webview() .notify_pointer_button_event(MouseWindowEvent::Click( MouseButton::Left, Point2D::new(x, y), )); self.maybe_perform_updates() } pub fn key_down(&mut self, key: Key) { let key_event = KeyboardEvent { state: KeyState::Down, key, ..KeyboardEvent::default() }; self.active_webview().notify_keyboard_event(key_event); self.maybe_perform_updates() } pub fn key_up(&mut self, key: Key) { let key_event = KeyboardEvent { state: KeyState::Up, key, ..KeyboardEvent::default() }; self.active_webview().notify_keyboard_event(key_event); self.maybe_perform_updates() } pub fn ime_insert_text(&mut self, text: String) { self.active_webview().notify_ime_event(CompositionEvent { state: CompositionState::End, data: text, }); self.maybe_perform_updates() } pub fn notify_vsync(&mut self) { self.active_webview().notify_vsync(); self.maybe_perform_updates() } pub fn pause_compositor(&mut self) { self.active_webview().invalidate_native_surface(); self.maybe_perform_updates(); } pub fn resume_compositor(&mut self, native_surface: *mut c_void, coords: Coordinates) { if native_surface.is_null() { panic!("null passed for native_surface"); } self.active_webview() .replace_native_surface(native_surface, coords.framebuffer); self.maybe_perform_updates() } pub fn media_session_action(&mut self, action: MediaSessionActionType) { info!("Media session action {:?}", action); self.active_webview() .notify_media_session_action_event(action); self.maybe_perform_updates() } pub fn set_throttled(&mut self, throttled: bool) { info!("set_throttled"); self.active_webview().set_throttled(throttled); self.maybe_perform_updates() } pub fn ime_dismissed(&mut self) { info!("ime_dismissed"); self.active_webview().notify_ime_dismissed_event(); self.maybe_perform_updates() } 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 maybe_perform_updates(&mut self) { self.perform_updates(); } fn handle_servo_events(&mut self) -> Result<(), &'static str> { let mut need_update = false; let messages: Vec<_> = self.servo.get_events().collect(); for message in messages { match message { EmbedderMsg::ChangePageTitle(_, title) => { self.callbacks.host_callbacks.on_title_changed(title); }, EmbedderMsg::AllowNavigationRequest(_, pipeline_id, url) => { let data: bool = self .callbacks .host_callbacks .on_allow_navigation(url.to_string()); self.servo.allow_navigation_response(pipeline_id, data); 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) => { cb.prompt_alert(message, trusted); sender.send(()) }, 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)) }, PromptDefinition::Credentials(_) => { warn!("implement credentials prompt for OpenHarmony OS and Android"); Ok(()) }, }; if let Err(e) = res { self.active_webview() .send_error(format!("Failed to send Prompt response: {e}")); } }, EmbedderMsg::AllowOpeningWebView(_, response_chan) => { let new_webview = self.servo.new_auxiliary_webview(); let new_webview_id = new_webview.id(); self.webviews.insert(new_webview_id, new_webview); self.creation_order.push(new_webview_id); if let Err(e) = response_chan.send(Some(new_webview_id)) { warn!("Failed to send AllowOpeningBrowser response: {}", e); }; }, EmbedderMsg::WebViewOpened(new_webview_id) => { if let Some(webview) = self.webviews.get(&new_webview_id) { webview.focus(); } }, 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) = self.newest_webview() { newest_webview.focus(); } else { self.servo.start_shutting_down(); } }, EmbedderMsg::WebViewFocused(webview_id) => { self.focused_webview_id = Some(webview_id); if let Some(webview) = self.webviews.get(&webview_id) { webview.show(true); } }, 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::RequestDevtoolsConnection(result_sender) => { result_sender.send(true); }, EmbedderMsg::Panic(_, reason, backtrace) => { self.callbacks.host_callbacks.on_panic(reason, backtrace); }, EmbedderMsg::ReadyToPresent(_webview_ids) => { self.need_present = true; }, EmbedderMsg::ResizeTo(_, size) => { warn!("Received resize event (to {size:?}). Currently only the user can resize windows"); }, EmbedderMsg::Keyboard(..) | EmbedderMsg::Status(..) | EmbedderMsg::SelectFiles(..) | EmbedderMsg::MoveTo(..) | EmbedderMsg::SetCursor(..) | EmbedderMsg::NewFavicon(..) | EmbedderMsg::HeadParsed(..) | EmbedderMsg::SetFullscreenState(..) | EmbedderMsg::ReportProfile(..) | EmbedderMsg::EventDelivered(..) | EmbedderMsg::PlayGamepadHapticEffect(..) | EmbedderMsg::StopGamepadHapticEffect(..) | EmbedderMsg::ClearClipboardContents(..) | EmbedderMsg::WebResourceRequested(..) => {}, } } if need_update { self.perform_updates(); } Ok(()) } pub fn present_if_needed(&mut self) { if self.need_present { self.need_present = false; self.servo.present(); } } } pub(super) struct ServoEmbedderCallbacks { waker: Box, #[cfg(feature = "webxr")] xr_discovery: Option, } impl ServoEmbedderCallbacks { pub(super) fn new( waker: Box, #[cfg(feature = "webxr")] xr_discovery: Option, ) -> Self { Self { waker, #[cfg(feature = "webxr")] xr_discovery, } } } impl EmbedderMethods for ServoEmbedderCallbacks { fn create_event_loop_waker(&mut self) -> Box { debug!("EmbedderMethods::create_event_loop_waker"); self.waker.clone() } #[cfg(feature = "webxr")] fn register_webxr( &mut self, registry: &mut servo::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(); let screen_size = (coords.viewport.size.to_f32() / self.hidpi_factor).to_i32(); EmbedderCoordinates { viewport: coords.viewport.to_box2d(), framebuffer: coords.framebuffer, window_rect: Box2D::from_origin_and_size(Point2D::zero(), screen_size), screen_size, available_screen_size: screen_size, hidpi_factor: self.hidpi_factor, } } fn set_animation_state(&self, state: AnimationState) { debug!("WindowMethods::set_animation_state: {:?}", state); self.host_callbacks .on_animating_changed(state == AnimationState::Animating); } }