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