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