servo/components/servo/webview.rs
Martin Robinson 2fe57cc2a2
libservo: Move animation tracking from WindowMethods to delegates (#36400)
This changes removes animation tracking from the `WindowMethods` trait
and moves it to `ServoDelegate` and `WebViewDelegate`.

- Animation changes per-`WebView` are now triggered in the compositor
  only when the value is updated there, rather than right after ticking
  animations.
- Both `WebView` and `Servo` now expose an `animation()` method, so
  tracking animation state actually becomes unecessary in many cases,
  such as that of desktop servoshell, which can just read the value
  when the event loop spins.

Testing: No tests necessary as the API layer is still untested. Later,
tests will be added for the `WebView` API and this can be tested then.
Signed-off-by: Martin Robinson <mrobinson@igalia.com>

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
2025-04-09 19:41:53 +00:00

510 lines
15 KiB
Rust

/* 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::{Ref, RefCell, RefMut};
use std::hash::Hash;
use std::rc::{Rc, Weak};
use std::time::Duration;
use base::id::WebViewId;
use compositing::IOCompositor;
use compositing::windowing::WebRenderDebugOption;
use compositing_traits::RendererWebView;
use constellation_traits::{EmbedderToConstellationMessage, TraversalDirection};
use dpi::PhysicalSize;
use embedder_traits::{
Cursor, InputEvent, LoadStatus, MediaSessionActionType, ScreenGeometry, Theme, TouchEventType,
};
use url::Url;
use webrender_api::ScrollLocation;
use webrender_api::units::{DeviceIntPoint, DeviceRect};
use crate::ConstellationProxy;
use crate::clipboard_delegate::{ClipboardDelegate, DefaultClipboardDelegate};
use crate::webview_delegate::{DefaultWebViewDelegate, WebViewDelegate};
/// A handle to a Servo webview. If you clone this handle, it does not create a new webview,
/// but instead creates a new handle to the webview. Once the last handle is dropped, Servo
/// considers that the webview has closed and will clean up all associated resources related
/// to this webview.
///
/// ## Rendering Model
///
/// Every [`WebView`] has a [`RenderingContext`](crate::RenderingContext). The embedder manages when
/// the contents of the [`WebView`] paint to the [`RenderingContext`](crate::RenderingContext). When
/// a [`WebView`] needs to be painted, for instance, because its contents have changed, Servo will
/// call [`WebViewDelegate::notify_new_frame_ready`] in order to signal that it is time to repaint
/// the [`WebView`] using [`WebView::paint`].
///
/// An example of how this flow might work is:
///
/// 1. [`WebViewDelegate::notify_new_frame_ready`] is called. The applications triggers a request
/// to repaint the window that contains this [`WebView`].
/// 2. During window repainting, the application calls [`WebView::paint`] and the contents of the
/// [`RenderingContext`][crate::RenderingContext] are updated.
/// 3. If the [`RenderingContext`][crate::RenderingContext] is double-buffered, the
/// application then calls [`crate::RenderingContext::present()`] in order to swap the back buffer
/// to the front, finally displaying the updated [`WebView`] contents.
///
/// In cases where the [`WebView`] contents have not been updated, but a repaint is necessary, for
/// instance when repainting a window due to damage, an application may simply perform the final two
/// steps and Servo will repaint even without first calling the
/// [`WebViewDelegate::notify_new_frame_ready`] method.
#[derive(Clone)]
pub struct WebView(Rc<RefCell<WebViewInner>>);
impl PartialEq for WebView {
fn eq(&self, other: &Self) -> bool {
self.inner().id == other.inner().id
}
}
impl Hash for WebView {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.inner().id.hash(state);
}
}
pub(crate) struct WebViewInner {
// TODO: ensure that WebView instances interact with the correct Servo instance
pub(crate) id: WebViewId,
pub(crate) constellation_proxy: ConstellationProxy,
pub(crate) compositor: Rc<RefCell<IOCompositor>>,
pub(crate) delegate: Rc<dyn WebViewDelegate>,
pub(crate) clipboard_delegate: Rc<dyn ClipboardDelegate>,
rect: DeviceRect,
load_status: LoadStatus,
url: Option<Url>,
status_text: Option<String>,
page_title: Option<String>,
favicon_url: Option<Url>,
focused: bool,
animating: bool,
cursor: Cursor,
}
impl Drop for WebViewInner {
fn drop(&mut self) {
self.constellation_proxy
.send(EmbedderToConstellationMessage::CloseWebView(self.id));
}
}
impl WebView {
pub(crate) fn new(
constellation_proxy: &ConstellationProxy,
compositor: Rc<RefCell<IOCompositor>>,
) -> Self {
let id = WebViewId::new();
let webview = Self(Rc::new(RefCell::new(WebViewInner {
id,
constellation_proxy: constellation_proxy.clone(),
compositor: compositor.clone(),
delegate: Rc::new(DefaultWebViewDelegate),
clipboard_delegate: Rc::new(DefaultClipboardDelegate),
rect: DeviceRect::zero(),
load_status: LoadStatus::Complete,
url: None,
status_text: None,
page_title: None,
favicon_url: None,
focused: false,
animating: false,
cursor: Cursor::Pointer,
})));
compositor
.borrow_mut()
.add_webview(Box::new(ServoRendererWebView {
weak_handle: webview.weak_handle(),
id,
}));
webview
}
fn inner(&self) -> Ref<'_, WebViewInner> {
self.0.borrow()
}
fn inner_mut(&self) -> RefMut<'_, WebViewInner> {
self.0.borrow_mut()
}
pub(crate) fn from_weak_handle(inner: &Weak<RefCell<WebViewInner>>) -> Option<Self> {
inner.upgrade().map(WebView)
}
pub(crate) fn weak_handle(&self) -> Weak<RefCell<WebViewInner>> {
Rc::downgrade(&self.0)
}
pub fn delegate(&self) -> Rc<dyn WebViewDelegate> {
self.inner().delegate.clone()
}
pub fn set_delegate(&self, delegate: Rc<dyn WebViewDelegate>) {
self.inner_mut().delegate = delegate;
}
pub fn clipboard_delegate(&self) -> Rc<dyn ClipboardDelegate> {
self.inner().clipboard_delegate.clone()
}
pub fn set_clipboard_delegate(&self, delegate: Rc<dyn ClipboardDelegate>) {
self.inner_mut().clipboard_delegate = delegate;
}
pub fn id(&self) -> WebViewId {
self.inner().id
}
pub fn load_status(&self) -> LoadStatus {
self.inner().load_status
}
pub(crate) fn set_load_status(self, new_value: LoadStatus) {
if self.inner().load_status == new_value {
return;
}
self.inner_mut().load_status = new_value;
self.delegate().notify_load_status_changed(self, new_value);
}
pub fn url(&self) -> Option<Url> {
self.inner().url.clone()
}
pub(crate) fn set_url(self, new_value: Url) {
if self
.inner()
.url
.as_ref()
.is_some_and(|url| url == &new_value)
{
return;
}
self.inner_mut().url = Some(new_value.clone());
self.delegate().notify_url_changed(self, new_value);
}
pub fn status_text(&self) -> Option<String> {
self.inner().status_text.clone()
}
pub(crate) fn set_status_text(self, new_value: Option<String>) {
if self.inner().status_text == new_value {
return;
}
self.inner_mut().status_text = new_value.clone();
self.delegate().notify_status_text_changed(self, new_value);
}
pub fn page_title(&self) -> Option<String> {
self.inner().page_title.clone()
}
pub(crate) fn set_page_title(self, new_value: Option<String>) {
if self.inner().page_title == new_value {
return;
}
self.inner_mut().page_title = new_value.clone();
self.delegate().notify_page_title_changed(self, new_value);
}
pub fn favicon_url(&self) -> Option<Url> {
self.inner().favicon_url.clone()
}
pub(crate) fn set_favicon_url(self, new_value: Url) {
if self
.inner()
.favicon_url
.as_ref()
.is_some_and(|url| url == &new_value)
{
return;
}
self.inner_mut().favicon_url = Some(new_value.clone());
self.delegate().notify_favicon_url_changed(self, new_value);
}
pub fn focused(&self) -> bool {
self.inner().focused
}
pub(crate) fn set_focused(self, new_value: bool) {
if self.inner().focused == new_value {
return;
}
self.inner_mut().focused = new_value;
self.delegate().notify_focus_changed(self, new_value);
}
pub fn cursor(&self) -> Cursor {
self.inner().cursor
}
pub(crate) fn set_cursor(self, new_value: Cursor) {
if self.inner().cursor == new_value {
return;
}
self.inner_mut().cursor = new_value;
self.delegate().notify_cursor_changed(self, new_value);
}
pub fn focus(&self) {
self.inner()
.constellation_proxy
.send(EmbedderToConstellationMessage::FocusWebView(self.id()));
}
pub fn blur(&self) {
self.inner()
.constellation_proxy
.send(EmbedderToConstellationMessage::BlurWebView);
}
/// Whether or not this [`WebView`] has animating content, such as a CSS animation or
/// transition or is running `requestAnimationFrame` callbacks. This indicates that the
/// embedding application should be spinning the Servo event loop on regular intervals
/// in order to trigger animation updates.
pub fn animating(self) -> bool {
self.inner().animating
}
pub(crate) fn set_animating(self, new_value: bool) {
if self.inner().animating == new_value {
return;
}
self.inner_mut().animating = new_value;
self.delegate().notify_animating_changed(self, new_value);
}
pub fn rect(&self) -> DeviceRect {
self.inner().rect
}
pub fn move_resize(&self, rect: DeviceRect) {
if self.inner().rect == rect {
return;
}
self.inner_mut().rect = rect;
self.inner()
.compositor
.borrow_mut()
.move_resize_webview(self.id(), rect);
}
pub fn show(&self, hide_others: bool) {
self.inner()
.compositor
.borrow_mut()
.show_webview(self.id(), hide_others)
.expect("BUG: invalid WebView instance");
}
pub fn hide(&self) {
self.inner()
.compositor
.borrow_mut()
.hide_webview(self.id())
.expect("BUG: invalid WebView instance");
}
pub fn raise_to_top(&self, hide_others: bool) {
self.inner()
.compositor
.borrow_mut()
.raise_webview_to_top(self.id(), hide_others)
.expect("BUG: invalid WebView instance");
}
pub fn notify_theme_change(&self, theme: Theme) {
self.inner()
.constellation_proxy
.send(EmbedderToConstellationMessage::ThemeChange(theme))
}
pub fn load(&self, url: Url) {
self.inner()
.constellation_proxy
.send(EmbedderToConstellationMessage::LoadUrl(
self.id(),
url.into(),
))
}
pub fn reload(&self) {
self.inner()
.constellation_proxy
.send(EmbedderToConstellationMessage::Reload(self.id()))
}
pub fn go_back(&self, amount: usize) {
self.inner()
.constellation_proxy
.send(EmbedderToConstellationMessage::TraverseHistory(
self.id(),
TraversalDirection::Back(amount),
))
}
pub fn go_forward(&self, amount: usize) {
self.inner()
.constellation_proxy
.send(EmbedderToConstellationMessage::TraverseHistory(
self.id(),
TraversalDirection::Forward(amount),
))
}
pub fn notify_scroll_event(
&self,
location: ScrollLocation,
point: DeviceIntPoint,
touch_event_action: TouchEventType,
) {
self.inner().compositor.borrow_mut().notify_scroll_event(
self.id(),
location,
point,
touch_event_action,
);
}
pub fn notify_input_event(&self, event: InputEvent) {
// Events with a `point` first go to the compositor for hit testing.
if event.point().is_some() {
self.inner()
.compositor
.borrow_mut()
.notify_input_event(self.id(), event);
return;
}
self.inner()
.constellation_proxy
.send(EmbedderToConstellationMessage::ForwardInputEvent(
self.id(),
event,
None, /* hit_test */
))
}
pub fn notify_media_session_action_event(&self, event: MediaSessionActionType) {
self.inner()
.constellation_proxy
.send(EmbedderToConstellationMessage::MediaSessionAction(event));
}
pub fn notify_vsync(&self) {
self.inner().compositor.borrow_mut().on_vsync(self.id());
}
pub fn resize(&self, new_size: PhysicalSize<u32>) {
self.inner()
.compositor
.borrow_mut()
.resize_rendering_context(new_size);
}
pub fn set_zoom(&self, new_zoom: f32) {
self.inner()
.compositor
.borrow_mut()
.on_zoom_window_event(new_zoom);
}
pub fn reset_zoom(&self) {
self.inner()
.compositor
.borrow_mut()
.on_zoom_reset_window_event();
}
pub fn set_pinch_zoom(&self, new_pinch_zoom: f32) {
self.inner()
.compositor
.borrow_mut()
.set_pinch_zoom(self.id(), new_pinch_zoom);
}
pub fn exit_fullscreen(&self) {
self.inner()
.constellation_proxy
.send(EmbedderToConstellationMessage::ExitFullScreen(self.id()));
}
pub fn set_throttled(&self, throttled: bool) {
self.inner()
.constellation_proxy
.send(EmbedderToConstellationMessage::SetWebViewThrottled(
self.id(),
throttled,
));
}
pub fn toggle_webrender_debugging(&self, debugging: WebRenderDebugOption) {
self.inner()
.compositor
.borrow_mut()
.toggle_webrender_debug(debugging);
}
pub fn capture_webrender(&self) {
self.inner().compositor.borrow_mut().capture_webrender();
}
pub fn toggle_sampling_profiler(&self, rate: Duration, max_duration: Duration) {
self.inner()
.constellation_proxy
.send(EmbedderToConstellationMessage::ToggleProfiler(
rate,
max_duration,
));
}
pub fn send_error(&self, message: String) {
self.inner()
.constellation_proxy
.send(EmbedderToConstellationMessage::SendError(
Some(self.id()),
message,
));
}
/// Paint the contents of this [`WebView`] into its `RenderingContext`. This will
/// always paint, unless the `Opts::wait_for_stable_image` option is enabled. In
/// that case, this might do nothing. Returns true if a paint was actually performed.
pub fn paint(&self) -> bool {
self.inner().compositor.borrow_mut().render()
}
}
/// A structure used to expose a view of the [`WebView`] to the Servo
/// renderer, without having the Servo renderer depend on the embedding layer.
struct ServoRendererWebView {
id: WebViewId,
weak_handle: Weak<RefCell<WebViewInner>>,
}
impl RendererWebView for ServoRendererWebView {
fn id(&self) -> WebViewId {
self.id
}
fn screen_geometry(&self) -> Option<ScreenGeometry> {
let webview = WebView::from_weak_handle(&self.weak_handle)?;
webview.delegate().screen_geometry(webview)
}
fn set_animating(&self, new_value: bool) {
if let Some(webview) = WebView::from_weak_handle(&self.weak_handle) {
webview.set_animating(new_value);
}
}
}