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