/* 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::{Cell, Ref, RefCell, RefMut}; use std::collections::HashMap; use std::rc::Rc; use dpi::PhysicalSize; use ipc_channel::ipc::IpcSender; use keyboard_types::{CompositionEvent, CompositionState}; use log::{debug, error, info, warn}; use raw_window_handle::{RawWindowHandle, WindowHandle}; use servo::base::id::WebViewId; use servo::euclid::{Point2D, Rect, Scale, Size2D, Vector2D}; use servo::servo_geometry::DeviceIndependentPixel; use servo::webrender_api::ScrollLocation; use servo::webrender_api::units::{DeviceIntRect, DeviceIntSize, DevicePixel}; use servo::{ AllowOrDenyRequest, ContextMenuResult, ImeEvent, InputEvent, InputMethodType, Key, KeyState, KeyboardEvent, LoadStatus, MediaSessionActionType, MediaSessionEvent, MouseButton, MouseButtonAction, MouseButtonEvent, MouseMoveEvent, NavigationRequest, PermissionRequest, RenderingContext, ScreenGeometry, Servo, ServoDelegate, ServoError, SimpleDialog, TouchEvent, TouchEventType, TouchId, WebView, WebViewBuilder, WebViewDelegate, WindowRenderingContext, }; use url::Url; use crate::egl::host_trait::HostTrait; use crate::prefs::ServoShellPreferences; #[derive(Clone, Debug)] pub struct Coordinates { pub viewport: Rect, } impl Coordinates { pub fn new(x: i32, y: i32, width: i32, height: i32) -> Coordinates { Coordinates { viewport: Rect::new(Point2D::new(x, y), Size2D::new(width, height)), } } pub fn origin(&self) -> Point2D { self.viewport.origin } pub fn size(&self) -> Size2D { self.viewport.size } } pub(super) struct ServoWindowCallbacks { host_callbacks: Box, coordinates: RefCell, } impl ServoWindowCallbacks { pub(super) fn new( host_callbacks: Box, coordinates: RefCell, ) -> Self { Self { host_callbacks, coordinates, } } } pub struct RunningAppState { servo: Servo, rendering_context: Rc, callbacks: Rc, inner: RefCell, /// servoshell specific preferences created during startup of the application. servoshell_preferences: ServoShellPreferences, } struct RunningAppStateInner { need_present: bool, /// 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, context_menu_sender: Option>, /// Whether or not the animation state has changed. This is used to trigger /// host callbacks indicating that animation state has changed. animating_state_changed: Rc>, /// The HiDPI scaling factor to use for the display of [`WebView`]s. hidpi_scale_factor: Scale, } struct ServoShellServoDelegate { animating_state_changed: Rc>, } impl ServoDelegate for ServoShellServoDelegate { fn notify_devtools_server_started(&self, _servo: &Servo, port: u16, _token: String) { info!("Devtools Server running on port {port}"); } fn request_devtools_connection(&self, _servo: &Servo, request: AllowOrDenyRequest) { request.allow(); } fn notify_error(&self, _servo: &Servo, error: ServoError) { error!("Saw Servo error: {error:?}!"); } fn notify_animating_changed(&self, _animating: bool) { self.animating_state_changed.set(true); } } impl WebViewDelegate for RunningAppState { fn screen_geometry(&self, _webview: WebView) -> Option { let coord = self.callbacks.coordinates.borrow(); let offset = coord.origin(); let available_size = coord.size(); let screen_size = coord.size(); Some(ScreenGeometry { size: screen_size, available_size, offset, }) } fn notify_page_title_changed(&self, _webview: servo::WebView, title: Option) { self.callbacks.host_callbacks.on_title_changed(title); } fn notify_history_changed(&self, _webview: WebView, entries: Vec, current: usize) { 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()); } fn notify_load_status_changed(&self, _webview: WebView, load_status: LoadStatus) { self.callbacks .host_callbacks .notify_load_status_changed(load_status); } fn notify_closed(&self, webview: WebView) { { let mut inner_mut = self.inner_mut(); inner_mut.webviews.retain(|&id, _| id != webview.id()); inner_mut.creation_order.retain(|&id| id != webview.id()); inner_mut.focused_webview_id = None; } if let Some(newest_webview) = self.newest_webview() { newest_webview.focus(); } else { self.servo.start_shutting_down(); } } fn notify_focus_changed(&self, webview: WebView, focused: bool) { if focused { self.inner_mut().focused_webview_id = Some(webview.id()); webview.show(true); } else if self.inner().focused_webview_id == Some(webview.id()) { self.inner_mut().focused_webview_id = None; } } fn notify_media_session_event(&self, _webview: WebView, event: MediaSessionEvent) { 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, ), }; } fn notify_crashed(&self, _webview: WebView, reason: String, backtrace: Option) { self.callbacks.host_callbacks.on_panic(reason, backtrace); } fn notify_new_frame_ready(&self, _webview: WebView) { self.inner_mut().need_present = true; } fn request_navigation(&self, _webview: WebView, navigation_request: NavigationRequest) { if self .callbacks .host_callbacks .on_allow_navigation(navigation_request.url.to_string()) { navigation_request.allow(); } else { navigation_request.deny(); } } fn request_open_auxiliary_webview(&self, parent_webview: WebView) -> Option { let webview = WebViewBuilder::new_auxiliary(&self.servo) .delegate(parent_webview.delegate()) .hidpi_scale_factor(self.inner().hidpi_scale_factor) .build(); self.add(webview.clone()); Some(webview) } fn request_permission(&self, webview: WebView, request: PermissionRequest) { self.callbacks .host_callbacks .request_permission(webview, request); } fn request_resize_to(&self, _webview: WebView, size: DeviceIntSize) { warn!("Received resize event (to {size:?}). Currently only the user can resize windows"); } fn show_context_menu( &self, _webview: WebView, result_sender: IpcSender, title: Option, items: Vec, ) { if self.inner().context_menu_sender.is_some() { warn!("Trying to show a context menu when a context menu is already active"); let _ = result_sender.send(ContextMenuResult::Ignored); } else { self.inner_mut().context_menu_sender = Some(result_sender); self.callbacks .host_callbacks .show_context_menu(title, items); } } fn show_simple_dialog(&self, webview: WebView, dialog: SimpleDialog) { self.callbacks .host_callbacks .show_simple_dialog(webview, dialog); } fn show_ime( &self, _webview: WebView, input_method_type: InputMethodType, text: Option<(String, i32)>, multiline: bool, position: DeviceIntRect, ) { self.callbacks .host_callbacks .on_ime_show(input_method_type, text, multiline, position); } fn hide_ime(&self, _webview: WebView) { self.callbacks.host_callbacks.on_ime_hide(); } } #[allow(unused)] impl RunningAppState { pub(super) fn new( initial_url: Option, hidpi_scale_factor: f32, rendering_context: Rc, servo: Servo, callbacks: Rc, servoshell_preferences: ServoShellPreferences, ) -> Rc { 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 animating_state_changed = Rc::new(Cell::new(false)); servo.set_delegate(Rc::new(ServoShellServoDelegate { animating_state_changed: animating_state_changed.clone(), })); let app_state = Rc::new(Self { rendering_context, servo, callbacks, servoshell_preferences, inner: RefCell::new(RunningAppStateInner { need_present: false, context_menu_sender: None, webviews: Default::default(), creation_order: vec![], focused_webview_id: None, animating_state_changed, hidpi_scale_factor: Scale::new(hidpi_scale_factor), }), }); app_state.new_toplevel_webview(initial_url); app_state } pub(crate) fn new_toplevel_webview(self: &Rc, url: Url) { let webview = WebViewBuilder::new(&self.servo) .url(url) .hidpi_scale_factor(self.inner().hidpi_scale_factor) .delegate(self.clone()) .build(); webview.focus(); self.add(webview.clone()); } pub(crate) fn add(&self, webview: WebView) { self.inner_mut().creation_order.push(webview.id()); self.inner_mut().webviews.insert(webview.id(), webview); } fn inner(&self) -> Ref { self.inner.borrow() } fn inner_mut(&self) -> RefMut { self.inner.borrow_mut() } fn get_browser_id(&self) -> Result { let webview_id = match self.inner().focused_webview_id { Some(id) => id, None => return Err("No focused WebViewId yet."), }; Ok(webview_id) } fn newest_webview(&self) -> Option { self.inner() .creation_order .last() .and_then(|id| self.inner().webviews.get(id).cloned()) } fn active_webview(&self) -> WebView { self.inner() .focused_webview_id .and_then(|id| self.inner().webviews.get(&id).cloned()) .or(self.newest_webview()) .expect("Should always have an active WebView") } /// Request shutdown. Will call on_shutdown_complete. pub fn request_shutdown(&self) { self.servo.start_shutting_down(); self.perform_updates(); } /// Call after on_shutdown_complete pub fn deinit(self) { self.servo.deinit(); } /// 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(&self) { let should_continue = self.servo.spin_event_loop(); if !should_continue { self.callbacks.host_callbacks.on_shutdown_complete(); } if self.inner().animating_state_changed.get() { self.inner().animating_state_changed.set(false); self.callbacks .host_callbacks .on_animating_changed(self.servo.animating()); } } /// Load an URL. pub fn load_uri(&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(&self) { info!("reload"); self.active_webview().reload(); self.perform_updates(); } /// Stop loading the page. pub fn stop(&self) { warn!("TODO can't stop won't stop"); } /// Go back in history. pub fn go_back(&self) { info!("go_back"); self.active_webview().go_back(1); self.perform_updates(); } /// Go forward in history. pub fn go_forward(&self) { info!("go_forward"); self.active_webview().go_forward(1); self.perform_updates(); } /// Let Servo know that the window has been resized. pub fn resize(&self, coordinates: Coordinates) { info!("resize to {:?}", coordinates,); self.active_webview().resize(PhysicalSize::new( coordinates.viewport.width() as u32, coordinates.viewport.height() as u32, )); *self.callbacks.coordinates.borrow_mut() = coordinates; self.perform_updates(); } /// Start scrolling. /// x/y are scroll coordinates. /// dx/dy are scroll deltas. #[cfg(not(target_env = "ohos"))] pub fn scroll_start(&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.perform_updates(); } /// Scroll. /// x/y are scroll coordinates. /// dx/dy are scroll deltas. pub fn scroll(&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.perform_updates(); } /// End scrolling. /// x/y are scroll coordinates. /// dx/dy are scroll deltas. #[cfg(not(target_env = "ohos"))] pub fn scroll_end(&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.perform_updates(); } /// Touch event: press down pub fn touch_down(&self, x: f32, y: f32, pointer_id: i32) { self.active_webview() .notify_input_event(InputEvent::Touch(TouchEvent::new( TouchEventType::Down, TouchId(pointer_id), Point2D::new(x, y), ))); self.perform_updates(); } /// Touch event: move touching finger pub fn touch_move(&self, x: f32, y: f32, pointer_id: i32) { self.active_webview() .notify_input_event(InputEvent::Touch(TouchEvent::new( TouchEventType::Move, TouchId(pointer_id), Point2D::new(x, y), ))); self.perform_updates(); } /// Touch event: Lift touching finger pub fn touch_up(&self, x: f32, y: f32, pointer_id: i32) { self.active_webview() .notify_input_event(InputEvent::Touch(TouchEvent::new( TouchEventType::Up, TouchId(pointer_id), Point2D::new(x, y), ))); self.perform_updates(); } /// Cancel touch event pub fn touch_cancel(&self, x: f32, y: f32, pointer_id: i32) { self.active_webview() .notify_input_event(InputEvent::Touch(TouchEvent::new( TouchEventType::Cancel, TouchId(pointer_id), Point2D::new(x, y), ))); self.perform_updates(); } /// Register a mouse movement. pub fn mouse_move(&self, x: f32, y: f32) { self.active_webview() .notify_input_event(InputEvent::MouseMove(MouseMoveEvent::new(Point2D::new( x, y, )))); self.perform_updates(); } /// Register a mouse button press. pub fn mouse_down(&self, x: f32, y: f32, button: MouseButton) { self.active_webview() .notify_input_event(InputEvent::MouseButton(MouseButtonEvent::new( MouseButtonAction::Down, button, Point2D::new(x, y), ))); self.perform_updates(); } /// Register a mouse button release. pub fn mouse_up(&self, x: f32, y: f32, button: MouseButton) { self.active_webview() .notify_input_event(InputEvent::MouseButton(MouseButtonEvent::new( MouseButtonAction::Up, button, Point2D::new(x, y), ))); self.perform_updates(); } /// Start pinchzoom. /// x/y are pinch origin coordinates. pub fn pinchzoom_start(&self, factor: f32, _x: u32, _y: u32) { self.active_webview().set_pinch_zoom(factor); self.perform_updates(); } /// Pinchzoom. /// x/y are pinch origin coordinates. pub fn pinchzoom(&self, factor: f32, _x: u32, _y: u32) { self.active_webview().set_pinch_zoom(factor); self.perform_updates(); } /// End pinchzoom. /// x/y are pinch origin coordinates. pub fn pinchzoom_end(&self, factor: f32, _x: u32, _y: u32) { self.active_webview().set_pinch_zoom(factor); self.perform_updates(); } /// Perform a click. pub fn click(&self, x: f32, y: f32) { self.active_webview() .notify_input_event(InputEvent::MouseButton(MouseButtonEvent::new( MouseButtonAction::Click, MouseButton::Left, Point2D::new(x, y), ))); self.perform_updates(); } pub fn key_down(&self, key: Key) { let key_event = KeyboardEvent { state: KeyState::Down, key, ..KeyboardEvent::default() }; self.active_webview() .notify_input_event(InputEvent::Keyboard(key_event)); self.perform_updates(); } pub fn key_up(&self, key: Key) { let key_event = KeyboardEvent { state: KeyState::Up, key, ..KeyboardEvent::default() }; self.active_webview() .notify_input_event(InputEvent::Keyboard(key_event)); self.perform_updates(); } pub fn ime_insert_text(&self, text: String) { self.active_webview() .notify_input_event(InputEvent::Ime(ImeEvent::Composition(CompositionEvent { state: CompositionState::End, data: text, }))); self.perform_updates(); } pub fn notify_vsync(&self) { self.active_webview().notify_vsync(); self.perform_updates(); } pub fn pause_compositor(&self) { if let Err(e) = self.rendering_context.take_window() { warn!("Unbinding native surface from context failed ({:?})", e); } self.perform_updates(); } pub fn resume_compositor(&self, window_handle: RawWindowHandle, coords: Coordinates) { let window_handle = unsafe { WindowHandle::borrow_raw(window_handle) }; let size = coords.viewport.size.to_u32(); if let Err(e) = self .rendering_context .set_window(window_handle, PhysicalSize::new(size.width, size.height)) { warn!("Binding native surface to context failed ({:?})", e); } self.perform_updates(); } pub fn media_session_action(&self, action: MediaSessionActionType) { info!("Media session action {:?}", action); self.active_webview() .notify_media_session_action_event(action); self.perform_updates(); } pub fn set_throttled(&self, throttled: bool) { info!("set_throttled"); self.active_webview().set_throttled(throttled); self.perform_updates(); } pub fn ime_dismissed(&self) { info!("ime_dismissed"); self.active_webview() .notify_input_event(InputEvent::Ime(ImeEvent::Dismissed)); self.perform_updates(); } pub fn on_context_menu_closed(&self, result: ContextMenuResult) -> Result<(), &'static str> { if let Some(sender) = self.inner_mut().context_menu_sender.take() { let _ = sender.send(result); } else { warn!("Trying to close a context menu when no context menu is active"); } Ok(()) } pub fn present_if_needed(&self) { if self.inner().need_present { self.inner_mut().need_present = false; self.active_webview().paint(); self.rendering_context.present(); } } } #[cfg(feature = "webxr")] pub(crate) struct XrDiscoveryWebXrRegistry { xr_discovery: RefCell>, } #[cfg(feature = "webxr")] impl XrDiscoveryWebXrRegistry { pub(crate) fn new(xr_discovery: Option) -> Self { Self { xr_discovery: RefCell::new(xr_discovery), } } } #[cfg(feature = "webxr")] impl servo::webxr::WebXrRegistry for XrDiscoveryWebXrRegistry { fn register(&self, registry: &mut servo::webxr::MainThreadRegistry) { debug!("XrDiscoveryWebXrRegistry::register"); if let Some(discovery) = self.xr_discovery.take() { registry.register(discovery); } } }