servo/
webview.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::{Ref, RefCell, RefMut};
6use std::hash::Hash;
7use std::rc::{Rc, Weak};
8use std::time::Duration;
9
10use base::id::WebViewId;
11use compositing::IOCompositor;
12use compositing_traits::WebViewTrait;
13use constellation_traits::{EmbedderToConstellationMessage, TraversalDirection};
14use dpi::PhysicalSize;
15use embedder_traits::{
16    Cursor, Image, InputEvent, InputEventAndId, InputEventId, JSValue, JavaScriptEvaluationError,
17    LoadStatus, MediaSessionActionType, ScreenGeometry, ScreenshotCaptureError, Scroll, Theme,
18    TraversalId, ViewportDetails, WebViewPoint, WebViewRect,
19};
20use euclid::{Point2D, Scale, Size2D};
21use image::RgbaImage;
22use servo_geometry::DeviceIndependentPixel;
23use style_traits::CSSPixel;
24use url::Url;
25use webrender_api::units::{DevicePixel, DevicePoint, DeviceRect};
26
27use crate::clipboard_delegate::{ClipboardDelegate, DefaultClipboardDelegate};
28use crate::javascript_evaluator::JavaScriptEvaluator;
29use crate::webview_delegate::{DefaultWebViewDelegate, WebViewDelegate};
30use crate::{ConstellationProxy, Servo, WebRenderDebugOption};
31
32pub(crate) const MINIMUM_WEBVIEW_SIZE: Size2D<i32, DevicePixel> = Size2D::new(1, 1);
33
34/// A handle to a Servo webview. If you clone this handle, it does not create a new webview,
35/// but instead creates a new handle to the webview. Once the last handle is dropped, Servo
36/// considers that the webview has closed and will clean up all associated resources related
37/// to this webview.
38///
39/// ## Rendering Model
40///
41/// Every [`WebView`] has a [`RenderingContext`](crate::RenderingContext). The embedder manages when
42/// the contents of the [`WebView`] paint to the [`RenderingContext`](crate::RenderingContext). When
43/// a [`WebView`] needs to be painted, for instance, because its contents have changed, Servo will
44/// call [`WebViewDelegate::notify_new_frame_ready`] in order to signal that it is time to repaint
45/// the [`WebView`] using [`WebView::paint`].
46///
47/// An example of how this flow might work is:
48///
49/// 1. [`WebViewDelegate::notify_new_frame_ready`] is called. The applications triggers a request
50///    to repaint the window that contains this [`WebView`].
51/// 2. During window repainting, the application calls [`WebView::paint`] and the contents of the
52///    [`RenderingContext`][crate::RenderingContext] are updated.
53/// 3. If the [`RenderingContext`][crate::RenderingContext] is double-buffered, the
54///    application then calls [`crate::RenderingContext::present()`] in order to swap the back buffer
55///    to the front, finally displaying the updated [`WebView`] contents.
56///
57/// In cases where the [`WebView`] contents have not been updated, but a repaint is necessary, for
58/// instance when repainting a window due to damage, an application may simply perform the final two
59/// steps and Servo will repaint even without first calling the
60/// [`WebViewDelegate::notify_new_frame_ready`] method.
61#[derive(Clone)]
62pub struct WebView(Rc<RefCell<WebViewInner>>);
63
64impl PartialEq for WebView {
65    fn eq(&self, other: &Self) -> bool {
66        self.inner().id == other.inner().id
67    }
68}
69
70impl Hash for WebView {
71    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
72        self.inner().id.hash(state);
73    }
74}
75
76pub(crate) struct WebViewInner {
77    // TODO: ensure that WebView instances interact with the correct Servo instance
78    pub(crate) id: WebViewId,
79    pub(crate) constellation_proxy: ConstellationProxy,
80    pub(crate) compositor: Rc<RefCell<IOCompositor>>,
81    pub(crate) delegate: Rc<dyn WebViewDelegate>,
82    pub(crate) clipboard_delegate: Rc<dyn ClipboardDelegate>,
83    javascript_evaluator: Rc<RefCell<JavaScriptEvaluator>>,
84
85    /// The rectangle of the [`WebView`] in device pixels, which is the viewport.
86    rect: DeviceRect,
87    hidpi_scale_factor: Scale<f32, DeviceIndependentPixel, DevicePixel>,
88    load_status: LoadStatus,
89    url: Option<Url>,
90    status_text: Option<String>,
91    page_title: Option<String>,
92    favicon: Option<Image>,
93    focused: bool,
94    animating: bool,
95    cursor: Cursor,
96}
97
98impl Drop for WebViewInner {
99    fn drop(&mut self) {
100        self.constellation_proxy
101            .send(EmbedderToConstellationMessage::CloseWebView(self.id));
102    }
103}
104
105impl WebView {
106    pub(crate) fn new(builder: WebViewBuilder) -> Self {
107        let compositor = builder.servo.compositor.clone();
108        let painter_id = compositor.borrow().painter_id();
109        let id = WebViewId::new(painter_id);
110        let servo = builder.servo;
111        let size = builder.size.map_or_else(
112            || {
113                builder
114                    .servo
115                    .compositor
116                    .borrow()
117                    .rendering_context_size()
118                    .to_f32()
119            },
120            |size| Size2D::new(size.width as f32, size.height as f32),
121        );
122
123        let webview = Self(Rc::new(RefCell::new(WebViewInner {
124            id,
125            constellation_proxy: servo.constellation_proxy.clone(),
126            compositor,
127            delegate: builder.delegate,
128            clipboard_delegate: Rc::new(DefaultClipboardDelegate),
129            javascript_evaluator: servo.javascript_evaluator.clone(),
130            rect: DeviceRect::from_origin_and_size(Point2D::origin(), size),
131            hidpi_scale_factor: builder.hidpi_scale_factor,
132            load_status: LoadStatus::Started,
133            url: None,
134            status_text: None,
135            page_title: None,
136            favicon: None,
137            focused: false,
138            animating: false,
139            cursor: Cursor::Pointer,
140        })));
141
142        let viewport_details = webview.viewport_details();
143        let wv = Box::new(ServoRendererWebView {
144            weak_handle: webview.weak_handle(),
145            id,
146        });
147        servo.compositor.borrow().add_webview(wv, viewport_details);
148
149        servo
150            .webviews
151            .borrow_mut()
152            .insert(webview.id(), webview.weak_handle());
153
154        if !builder.auxiliary {
155            let url = builder.url.unwrap_or(
156                Url::parse("about:blank").expect("Should always be able to parse 'about:blank'."),
157            );
158
159            builder
160                .servo
161                .constellation_proxy
162                .send(EmbedderToConstellationMessage::NewWebView(
163                    url.into(),
164                    webview.id(),
165                    viewport_details,
166                ));
167        }
168
169        webview
170    }
171
172    fn inner(&self) -> Ref<'_, WebViewInner> {
173        self.0.borrow()
174    }
175
176    fn inner_mut(&self) -> RefMut<'_, WebViewInner> {
177        self.0.borrow_mut()
178    }
179
180    pub(crate) fn viewport_details(&self) -> ViewportDetails {
181        // The division by 1 represents the page's default zoom of 100%,
182        // and gives us the appropriate CSSPixel type for the viewport.
183        let inner = self.inner();
184        let scaled_viewport_size = inner.rect.size() / inner.hidpi_scale_factor;
185        ViewportDetails {
186            size: scaled_viewport_size / Scale::new(1.0),
187            hidpi_scale_factor: Scale::new(inner.hidpi_scale_factor.0),
188        }
189    }
190
191    pub(crate) fn from_weak_handle(inner: &Weak<RefCell<WebViewInner>>) -> Option<Self> {
192        inner.upgrade().map(WebView)
193    }
194
195    pub(crate) fn weak_handle(&self) -> Weak<RefCell<WebViewInner>> {
196        Rc::downgrade(&self.0)
197    }
198
199    pub fn delegate(&self) -> Rc<dyn WebViewDelegate> {
200        self.inner().delegate.clone()
201    }
202
203    pub fn set_delegate(&self, delegate: Rc<dyn WebViewDelegate>) {
204        self.inner_mut().delegate = delegate;
205    }
206
207    pub fn clipboard_delegate(&self) -> Rc<dyn ClipboardDelegate> {
208        self.inner().clipboard_delegate.clone()
209    }
210
211    pub fn set_clipboard_delegate(&self, delegate: Rc<dyn ClipboardDelegate>) {
212        self.inner_mut().clipboard_delegate = delegate;
213    }
214
215    pub fn id(&self) -> WebViewId {
216        self.inner().id
217    }
218
219    pub fn load_status(&self) -> LoadStatus {
220        self.inner().load_status
221    }
222
223    pub(crate) fn set_load_status(self, new_value: LoadStatus) {
224        if self.inner().load_status == new_value {
225            return;
226        }
227        self.inner_mut().load_status = new_value;
228        self.delegate().notify_load_status_changed(self, new_value);
229    }
230
231    pub fn url(&self) -> Option<Url> {
232        self.inner().url.clone()
233    }
234
235    pub(crate) fn set_url(self, new_value: Url) {
236        if self
237            .inner()
238            .url
239            .as_ref()
240            .is_some_and(|url| url == &new_value)
241        {
242            return;
243        }
244        self.inner_mut().url = Some(new_value.clone());
245        self.delegate().notify_url_changed(self, new_value);
246    }
247
248    pub fn status_text(&self) -> Option<String> {
249        self.inner().status_text.clone()
250    }
251
252    pub(crate) fn set_status_text(self, new_value: Option<String>) {
253        if self.inner().status_text == new_value {
254            return;
255        }
256        self.inner_mut().status_text = new_value.clone();
257        self.delegate().notify_status_text_changed(self, new_value);
258    }
259
260    pub fn page_title(&self) -> Option<String> {
261        self.inner().page_title.clone()
262    }
263
264    pub(crate) fn set_page_title(self, new_value: Option<String>) {
265        if self.inner().page_title == new_value {
266            return;
267        }
268        self.inner_mut().page_title = new_value.clone();
269        self.delegate().notify_page_title_changed(self, new_value);
270    }
271
272    pub fn favicon(&self) -> Option<Ref<'_, Image>> {
273        Ref::filter_map(self.inner(), |inner| inner.favicon.as_ref()).ok()
274    }
275
276    pub(crate) fn set_favicon(self, new_value: Image) {
277        self.inner_mut().favicon = Some(new_value);
278        self.delegate().notify_favicon_changed(self);
279    }
280
281    pub fn focused(&self) -> bool {
282        self.inner().focused
283    }
284
285    pub(crate) fn set_focused(self, new_value: bool) {
286        if self.inner().focused == new_value {
287            return;
288        }
289        self.inner_mut().focused = new_value;
290        self.delegate().notify_focus_changed(self, new_value);
291    }
292
293    pub fn cursor(&self) -> Cursor {
294        self.inner().cursor
295    }
296
297    pub(crate) fn set_cursor(self, new_value: Cursor) {
298        if self.inner().cursor == new_value {
299            return;
300        }
301        self.inner_mut().cursor = new_value;
302        self.delegate().notify_cursor_changed(self, new_value);
303    }
304
305    pub fn focus(&self) {
306        self.inner()
307            .constellation_proxy
308            .send(EmbedderToConstellationMessage::FocusWebView(self.id()));
309    }
310
311    pub fn blur(&self) {
312        self.inner()
313            .constellation_proxy
314            .send(EmbedderToConstellationMessage::BlurWebView);
315    }
316
317    /// Whether or not this [`WebView`] has animating content, such as a CSS animation or
318    /// transition or is running `requestAnimationFrame` callbacks. This indicates that the
319    /// embedding application should be spinning the Servo event loop on regular intervals
320    /// in order to trigger animation updates.
321    pub fn animating(self) -> bool {
322        self.inner().animating
323    }
324
325    pub(crate) fn set_animating(self, new_value: bool) {
326        if self.inner().animating == new_value {
327            return;
328        }
329        self.inner_mut().animating = new_value;
330        self.delegate().notify_animating_changed(self, new_value);
331    }
332
333    pub fn rect(&self) -> DeviceRect {
334        self.inner().rect
335    }
336
337    pub fn move_resize(&self, rect: DeviceRect) {
338        if self.inner().rect == rect {
339            return;
340        }
341
342        let rect =
343            DeviceRect::from_origin_and_size(rect.min, rect.size().max(Size2D::new(1.0, 1.0)));
344
345        self.inner_mut().rect = rect;
346        self.inner()
347            .compositor
348            .borrow()
349            .move_resize_webview(self.id(), rect);
350    }
351
352    /// Request that the given [`WebView`]'s rendering area be resized. Note that the
353    /// minimum size for a WebView is 1 pixel by 1 pixel so any requested size will be
354    /// clamped by that value.
355    pub fn resize(&self, new_size: PhysicalSize<u32>) {
356        let new_size = PhysicalSize {
357            width: new_size.width.max(MINIMUM_WEBVIEW_SIZE.width as u32),
358            height: new_size.height.max(MINIMUM_WEBVIEW_SIZE.height as u32),
359        };
360
361        self.inner()
362            .compositor
363            .borrow()
364            .resize_rendering_context(new_size);
365    }
366
367    pub fn hidpi_scale_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel> {
368        self.inner().hidpi_scale_factor
369    }
370
371    pub fn set_hidpi_scale_factor(
372        &self,
373        new_scale_factor: Scale<f32, DeviceIndependentPixel, DevicePixel>,
374    ) {
375        if self.inner().hidpi_scale_factor == new_scale_factor {
376            return;
377        }
378
379        self.inner_mut().hidpi_scale_factor = new_scale_factor;
380        self.inner()
381            .compositor
382            .borrow()
383            .set_hidpi_scale_factor(self.id(), new_scale_factor);
384    }
385
386    pub fn show(&self, hide_others: bool) {
387        self.inner()
388            .compositor
389            .borrow()
390            .show_webview(self.id(), hide_others)
391            .expect("BUG: invalid WebView instance");
392    }
393
394    pub fn hide(&self) {
395        self.inner()
396            .compositor
397            .borrow()
398            .hide_webview(self.id())
399            .expect("BUG: invalid WebView instance");
400    }
401
402    pub fn raise_to_top(&self, hide_others: bool) {
403        self.inner()
404            .compositor
405            .borrow()
406            .raise_webview_to_top(self.id(), hide_others)
407            .expect("BUG: invalid WebView instance");
408    }
409
410    pub fn focus_and_raise_to_top(&self, hide_others: bool) {
411        self.focus();
412        self.raise_to_top(hide_others);
413    }
414
415    pub fn notify_theme_change(&self, theme: Theme) {
416        self.inner()
417            .constellation_proxy
418            .send(EmbedderToConstellationMessage::ThemeChange(
419                self.id(),
420                theme,
421            ))
422    }
423
424    pub fn load(&self, url: Url) {
425        self.inner()
426            .constellation_proxy
427            .send(EmbedderToConstellationMessage::LoadUrl(
428                self.id(),
429                url.into(),
430            ))
431    }
432
433    pub fn reload(&self) {
434        self.inner()
435            .constellation_proxy
436            .send(EmbedderToConstellationMessage::Reload(self.id()))
437    }
438
439    pub fn go_back(&self, amount: usize) -> TraversalId {
440        let traversal_id = TraversalId::new();
441        self.inner()
442            .constellation_proxy
443            .send(EmbedderToConstellationMessage::TraverseHistory(
444                self.id(),
445                TraversalDirection::Back(amount),
446                traversal_id.clone(),
447            ));
448        traversal_id
449    }
450
451    pub fn go_forward(&self, amount: usize) -> TraversalId {
452        let traversal_id = TraversalId::new();
453        self.inner()
454            .constellation_proxy
455            .send(EmbedderToConstellationMessage::TraverseHistory(
456                self.id(),
457                TraversalDirection::Forward(amount),
458                traversal_id.clone(),
459            ));
460        traversal_id
461    }
462
463    /// Ask the [`WebView`] to scroll web content. Note that positive scroll offsets reveal more
464    /// content on the bottom and right of the page.
465    pub fn notify_scroll_event(&self, scroll: Scroll, point: WebViewPoint) {
466        self.inner()
467            .compositor
468            .borrow()
469            .notify_scroll_event(self.id(), scroll, point);
470    }
471
472    pub fn notify_input_event(&self, event: InputEvent) -> InputEventId {
473        let event: InputEventAndId = event.into();
474        let event_id = event.id;
475
476        // Events with a `point` first go to the compositor for hit testing.
477        if event.event.point().is_some() {
478            self.inner()
479                .compositor
480                .borrow()
481                .notify_input_event(self.id(), event);
482        } else {
483            self.inner().constellation_proxy.send(
484                EmbedderToConstellationMessage::ForwardInputEvent(
485                    self.id(),
486                    event,
487                    None, /* hit_test */
488                ),
489            );
490        }
491
492        event_id
493    }
494
495    pub fn notify_media_session_action_event(&self, event: MediaSessionActionType) {
496        self.inner()
497            .constellation_proxy
498            .send(EmbedderToConstellationMessage::MediaSessionAction(event));
499    }
500
501    /// Set the page zoom of the [`WebView`]. This sets the final page zoom value of the
502    /// [`WebView`]. Unlike [`WebView::pinch_zoom`] *it is not* multiplied by the current
503    /// page zoom value, but overrides it.
504    ///
505    /// [`WebView`]s have two types of zoom, pinch zoom and page zoom. This adjusts page
506    /// zoom, which will adjust the `devicePixelRatio` of the page and cause it to modify
507    /// its layout.
508    ///
509    /// These values will be clamped internally. The values used for clamping can be
510    /// adjusted by page content when `<meta viewport>` parsing is enabled via
511    /// `Prefs::viewport_meta_enabled`.
512    pub fn set_page_zoom(&self, new_zoom: f32) {
513        self.inner()
514            .compositor
515            .borrow()
516            .set_page_zoom(self.id(), new_zoom);
517    }
518
519    /// Get the page zoom of the [`WebView`].
520    pub fn page_zoom(&self) -> f32 {
521        self.inner().compositor.borrow().page_zoom(self.id())
522    }
523
524    /// Adjust the pinch zoom on this [`WebView`] multiplying the current pinch zoom
525    /// level with the provided `pinch_zoom_delta`.
526    ///
527    /// [`WebView`]s have two types of zoom, pinch zoom and page zoom. This adjusts pinch
528    /// zoom, which is a type of zoom which does not modify layout, and instead simply
529    /// magnifies the view in the viewport.
530    ///
531    /// The final pinch zoom values will be clamped to reasonable defaults (currently to
532    /// the inclusive range [1.0, 10.0]).
533    pub fn pinch_zoom(&self, pinch_zoom_delta: f32, center: DevicePoint) {
534        self.inner()
535            .compositor
536            .borrow()
537            .pinch_zoom(self.id(), pinch_zoom_delta, center);
538    }
539
540    pub fn device_pixels_per_css_pixel(&self) -> Scale<f32, CSSPixel, DevicePixel> {
541        self.inner()
542            .compositor
543            .borrow()
544            .device_pixels_per_page_pixel(self.id())
545    }
546
547    pub fn exit_fullscreen(&self) {
548        self.inner()
549            .constellation_proxy
550            .send(EmbedderToConstellationMessage::ExitFullScreen(self.id()));
551    }
552
553    pub fn set_throttled(&self, throttled: bool) {
554        self.inner()
555            .constellation_proxy
556            .send(EmbedderToConstellationMessage::SetWebViewThrottled(
557                self.id(),
558                throttled,
559            ));
560    }
561
562    pub fn toggle_webrender_debugging(&self, debugging: WebRenderDebugOption) {
563        self.inner()
564            .compositor
565            .borrow()
566            .toggle_webrender_debug(debugging);
567    }
568
569    pub fn capture_webrender(&self) {
570        self.inner().compositor.borrow().capture_webrender();
571    }
572
573    pub fn toggle_sampling_profiler(&self, rate: Duration, max_duration: Duration) {
574        self.inner()
575            .constellation_proxy
576            .send(EmbedderToConstellationMessage::ToggleProfiler(
577                rate,
578                max_duration,
579            ));
580    }
581
582    pub fn send_error(&self, message: String) {
583        self.inner()
584            .constellation_proxy
585            .send(EmbedderToConstellationMessage::SendError(
586                Some(self.id()),
587                message,
588            ));
589    }
590
591    /// Paint the contents of this [`WebView`] into its `RenderingContext`.
592    pub fn paint(&self) {
593        self.inner().compositor.borrow().render();
594    }
595
596    /// Evaluate the specified string of JavaScript code. Once execution is complete or an error
597    /// occurs, Servo will call `callback`.
598    pub fn evaluate_javascript<T: ToString>(
599        &self,
600        script: T,
601        callback: impl FnOnce(Result<JSValue, JavaScriptEvaluationError>) + 'static,
602    ) {
603        self.inner().javascript_evaluator.borrow_mut().evaluate(
604            self.id(),
605            script.to_string(),
606            Box::new(callback),
607        );
608    }
609
610    /// Asynchronously take a screenshot of the [`WebView`] contents, given a `rect` or the whole
611    /// viewport, if no `rect` is given.
612    ///
613    /// This method will wait until the [`WebView`] is ready before the screenshot is taken.
614    /// This includes waiting for:
615    ///
616    ///  - all frames to fire their `load` event.
617    ///  - all render blocking elements, such as stylesheets included via the `<link>`
618    ///    element, to stop blocking the rendering.
619    ///  - all images to be loaded and displayed.
620    ///  - all web fonts are loaded.
621    ///  - the `reftest-wait` and `test-wait` classes have been removed from the root element.
622    ///  - the rendering is up-to-date
623    ///
624    /// Once all these conditions are met and the rendering does not have any pending frames
625    /// to render, the provided `callback` will be called with the results of the screenshot
626    /// operation.
627    pub fn take_screenshot(
628        &self,
629        rect: Option<WebViewRect>,
630        callback: impl FnOnce(Result<RgbaImage, ScreenshotCaptureError>) + 'static,
631    ) {
632        self.inner()
633            .compositor
634            .borrow()
635            .request_screenshot(self.id(), rect, Box::new(callback));
636    }
637}
638
639/// A structure used to expose a view of the [`WebView`] to the Servo
640/// renderer, without having the Servo renderer depend on the embedding layer.
641struct ServoRendererWebView {
642    id: WebViewId,
643    weak_handle: Weak<RefCell<WebViewInner>>,
644}
645
646impl WebViewTrait for ServoRendererWebView {
647    fn id(&self) -> WebViewId {
648        self.id
649    }
650
651    fn screen_geometry(&self) -> Option<ScreenGeometry> {
652        let webview = WebView::from_weak_handle(&self.weak_handle)?;
653        webview.delegate().screen_geometry(webview)
654    }
655
656    fn set_animating(&self, new_value: bool) {
657        if let Some(webview) = WebView::from_weak_handle(&self.weak_handle) {
658            webview.set_animating(new_value);
659        }
660    }
661}
662
663pub struct WebViewBuilder<'servo> {
664    servo: &'servo Servo,
665    delegate: Rc<dyn WebViewDelegate>,
666    auxiliary: bool,
667    url: Option<Url>,
668    size: Option<PhysicalSize<u32>>,
669    hidpi_scale_factor: Scale<f32, DeviceIndependentPixel, DevicePixel>,
670}
671
672impl<'servo> WebViewBuilder<'servo> {
673    pub fn new(servo: &'servo Servo) -> Self {
674        Self {
675            servo,
676            auxiliary: false,
677            url: None,
678            size: None,
679            hidpi_scale_factor: Scale::new(1.0),
680            delegate: Rc::new(DefaultWebViewDelegate),
681        }
682    }
683
684    pub fn new_auxiliary(servo: &'servo Servo) -> Self {
685        let mut builder = Self::new(servo);
686        builder.auxiliary = true;
687        builder
688    }
689
690    pub fn delegate(mut self, delegate: Rc<dyn WebViewDelegate>) -> Self {
691        self.delegate = delegate;
692        self
693    }
694
695    pub fn url(mut self, url: Url) -> Self {
696        self.url = Some(url);
697        self
698    }
699
700    pub fn size(mut self, size: PhysicalSize<u32>) -> Self {
701        self.size = Some(size);
702        self
703    }
704
705    pub fn hidpi_scale_factor(
706        mut self,
707        hidpi_scale_factor: Scale<f32, DeviceIndependentPixel, DevicePixel>,
708    ) -> Self {
709        self.hidpi_scale_factor = hidpi_scale_factor;
710        self
711    }
712
713    pub fn build(self) -> WebView {
714        WebView::new(self)
715    }
716}