Skip to main content

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::{Cell, Ref, RefCell, RefMut};
6use std::hash::Hash;
7use std::rc::{Rc, Weak};
8
9use accesskit::{
10    Affine as AccesskitAffine, Node as AccesskitNode, NodeId, Rect as AccesskitRect, Role, Tree,
11    TreeId, TreeUpdate, Uuid as AccesskitUuid,
12};
13use dpi::PhysicalSize;
14use embedder_traits::{
15    ContextMenuAction, ContextMenuItem, Cursor, EmbedderControlId, EmbedderControlRequest, Image,
16    InputEvent, InputEventAndId, InputEventId, JSValue, JavaScriptEvaluationError, LoadStatus,
17    MediaSessionActionType, NewWebViewDetails, ScreenGeometry, ScreenshotCaptureError, Scroll,
18    Theme, TraversalId, UrlRequest, ViewportDetails, WebViewPoint, WebViewRect,
19};
20use euclid::{Scale, Size2D};
21use image::RgbaImage;
22use log::debug;
23use paint_api::WebViewTrait;
24use paint_api::rendering_context::RenderingContext;
25use servo_base::Epoch;
26use servo_base::generic_channel::GenericSender;
27use servo_base::id::WebViewId;
28use servo_config::pref;
29use servo_constellation_traits::{
30    EmbedderToConstellationMessage, HistoryTraversalSource, SessionHistoryTraversalRequest,
31    TraversalDirection,
32};
33use servo_geometry::DeviceIndependentPixel;
34use servo_url::ServoUrl;
35use style_traits::CSSPixel;
36use url::Url;
37use webrender_api::units::{DeviceIntRect, DevicePixel, DevicePoint, DeviceSize};
38
39use crate::clipboard_delegate::{ClipboardDelegate, DefaultClipboardDelegate};
40#[cfg(feature = "gamepad")]
41use crate::gamepad_delegate::{DefaultGamepadDelegate, GamepadDelegate};
42use crate::geolocation_delegate::{DefaultGeolocationDelegate, GeolocationDelegate};
43use crate::responders::AutomaticResponder;
44use crate::servo::PendingHandledInputEvent;
45use crate::webview_delegate::{CreateNewWebViewRequest, DefaultWebViewDelegate, WebViewDelegate};
46use crate::{
47    ColorPicker, ContextMenu, EmbedderControl, InputMethodControl, SelectElement, Servo,
48    UserContentManager, WebRenderDebugOption,
49};
50
51pub(crate) const MINIMUM_WEBVIEW_SIZE: Size2D<i32, DevicePixel> = Size2D::new(1, 1);
52
53/// A handle to a Servo webview. If you clone this handle, it does not create a new webview,
54/// but instead creates a new handle to the webview. Once the last handle is dropped, Servo
55/// considers that the webview has closed and will clean up all associated resources related
56/// to this webview.
57///
58/// ## Creating a WebView
59///
60/// To create a [`WebView`], use [`WebViewBuilder`].
61///
62/// ## Rendering Model
63///
64/// Every [`WebView`] has a [`RenderingContext`]. The embedder manages when
65/// the contents of the [`WebView`] paint to the [`RenderingContext`]. When
66/// a [`WebView`] needs to be painted, for instance, because its contents have changed, Servo will
67/// call [`WebViewDelegate::notify_new_frame_ready`] in order to signal that it is time to repaint
68/// the [`WebView`] using [`WebView::paint`].
69///
70/// An example of how this flow might work is:
71///
72/// 1. [`WebViewDelegate::notify_new_frame_ready`] is called. The applications triggers a request
73///    to repaint the window that contains this [`WebView`].
74/// 2. During window repainting, the application calls [`WebView::paint`] and the contents of the
75///    [`RenderingContext`] are updated.
76/// 3. If the [`RenderingContext`] is double-buffered, the
77///    application then calls [`crate::RenderingContext::present()`] in order to swap the back buffer
78///    to the front, finally displaying the updated [`WebView`] contents.
79///
80/// In cases where the [`WebView`] contents have not been updated, but a repaint is necessary, for
81/// instance when repainting a window due to damage, an application may simply perform the final two
82/// steps and Servo will repaint even without first calling the
83/// [`WebViewDelegate::notify_new_frame_ready`] method.
84#[derive(Clone)]
85pub struct WebView(Rc<RefCell<WebViewInner>>);
86
87impl PartialEq for WebView {
88    fn eq(&self, other: &Self) -> bool {
89        self.inner().id == other.inner().id
90    }
91}
92
93impl Hash for WebView {
94    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
95        self.inner().id.hash(state);
96    }
97}
98
99pub(crate) struct WebViewInner {
100    pub(crate) id: WebViewId,
101    pub(crate) servo: Servo,
102    pub(crate) delegate: Rc<dyn WebViewDelegate>,
103    pub(crate) clipboard_delegate: Rc<dyn ClipboardDelegate>,
104    #[cfg(feature = "gamepad")]
105    pub(crate) gamepad_delegate: Rc<dyn GamepadDelegate>,
106    pub(crate) geolocation_delegate: Rc<dyn GeolocationDelegate>,
107
108    /// AccessKit subtree id for this [`WebView`], if accessibility is active.
109    ///
110    /// Set by [`WebView::set_accessibility_active()`], and forwarded to the constellation via
111    /// [`EmbedderToConstellationMessage::SetAccessibilityActive`].
112    pub(crate) accesskit_tree_id: Option<TreeId>,
113    /// [`TreeId`] of the web contents of this [`WebView`]’s active top-level pipeline,
114    /// which is grafted into the tree for this [`WebView`].
115    pub(crate) grafted_accesskit_tree_id: Option<TreeId>,
116    /// A counter for changes to the grafted accesskit tree for this webview.
117    /// See [`Self::grafted_accesskit_tree_id`].
118    grafted_accesskit_tree_epoch: Option<Epoch>,
119    /// Set when the paint layer applies a change to this [`WebView`]'s accessibility viewport
120    /// geometry — its size, page or pinch zoom, or HiDPI scale — via
121    /// [`WebViewTrait::notify_viewport_updated()`]. The root accessibility node is re-sent from
122    /// [`Servo`]'s event loop rather than from there, so that nothing calls into the
123    /// [`WebViewDelegate`] while the paint `RefCell` (or an embedder-facing method) is on the stack,
124    /// which could cause re-entrant `RefCell` borrows. See
125    /// [`WebView::note_accessibility_viewport_changed()`].
126    accessibility_viewport_changed: Cell<bool>,
127
128    rendering_context: Rc<dyn RenderingContext>,
129    user_content_manager: Option<Rc<UserContentManager>>,
130    hidpi_scale_factor: Scale<f32, DeviceIndependentPixel, DevicePixel>,
131    load_status: LoadStatus,
132    status_text: Option<String>,
133    page_title: Option<String>,
134    favicon: Option<Image>,
135    focused: bool,
136    animating: bool,
137    cursor: Cursor,
138
139    /// The back / forward list of this WebView.
140    back_forward_list: Vec<Url>,
141
142    /// The current index in the back / forward list.
143    back_forward_list_index: usize,
144}
145
146impl Drop for WebViewInner {
147    fn drop(&mut self) {
148        self.servo
149            .constellation_proxy()
150            .send(EmbedderToConstellationMessage::CloseWebView(self.id));
151        self.servo.paint_mut().remove_webview(self.id);
152    }
153}
154
155impl WebView {
156    pub(crate) fn new(mut builder: WebViewBuilder) -> Self {
157        let servo = builder.servo;
158        let painter_id = servo
159            .paint_mut()
160            .register_rendering_context(builder.rendering_context.clone());
161
162        let id = WebViewId::new(painter_id);
163        let webview = Self(Rc::new(RefCell::new(WebViewInner {
164            id,
165            servo: servo.clone(),
166            rendering_context: builder.rendering_context,
167            delegate: builder.delegate,
168            clipboard_delegate: builder
169                .clipboard_delegate
170                .unwrap_or_else(|| Rc::new(DefaultClipboardDelegate)),
171            #[cfg(feature = "gamepad")]
172            gamepad_delegate: builder
173                .gamepad_delegate
174                .unwrap_or_else(|| Rc::new(DefaultGamepadDelegate)),
175            geolocation_delegate: builder
176                .geolocation_delegate
177                .unwrap_or_else(|| Rc::new(DefaultGeolocationDelegate)),
178            accesskit_tree_id: None,
179            grafted_accesskit_tree_id: None,
180            grafted_accesskit_tree_epoch: None,
181            accessibility_viewport_changed: Cell::new(false),
182            hidpi_scale_factor: builder.hidpi_scale_factor,
183            load_status: LoadStatus::Started,
184            status_text: None,
185            page_title: None,
186            favicon: None,
187            focused: true,
188            animating: false,
189            cursor: Cursor::Pointer,
190            back_forward_list: Default::default(),
191            back_forward_list_index: 0,
192            user_content_manager: builder.user_content_manager.clone(),
193        })));
194
195        let viewport_details = webview.viewport_details();
196        servo.paint().add_webview(
197            Box::new(ServoRendererWebView {
198                weak_handle: webview.weak_handle(),
199                id,
200            }),
201            viewport_details,
202        );
203
204        servo
205            .webviews_mut()
206            .insert(webview.id(), webview.weak_handle());
207
208        let user_content_manager_id = builder
209            .user_content_manager
210            .as_ref()
211            .map(|user_content_manager| user_content_manager.id());
212
213        let new_webview_details = NewWebViewDetails {
214            webview_id: webview.id(),
215            viewport_details,
216            user_content_manager_id,
217        };
218
219        // There are two possibilities here. Either the WebView is a new toplevel
220        // WebView in which case `Self::create_new_webview_responder` is `None` or this
221        // is the response to a `WebViewDelegate::request_create_new` method in which
222        // case script expects that we just return the information directly back to
223        // the `ScriptThread`.
224        match builder.create_new_webview_responder.as_mut() {
225            Some(responder) => {
226                let _ = responder.send(Some(new_webview_details));
227            },
228            None => {
229                let url = builder.url.unwrap_or(
230                    Url::parse("about:blank")
231                        .expect("Should always be able to parse 'about:blank'."),
232                );
233
234                servo
235                    .constellation_proxy()
236                    .send(EmbedderToConstellationMessage::NewWebView(
237                        url.into(),
238                        new_webview_details,
239                    ));
240            },
241        }
242
243        webview
244    }
245
246    fn inner(&self) -> Ref<'_, WebViewInner> {
247        self.0.borrow()
248    }
249
250    fn inner_mut(&self) -> RefMut<'_, WebViewInner> {
251        self.0.borrow_mut()
252    }
253
254    fn servo(&self) -> Servo {
255        self.0.borrow().servo.clone()
256    }
257
258    pub(crate) fn request_create_new(
259        &self,
260        response_sender: GenericSender<Option<NewWebViewDetails>>,
261    ) {
262        let request = CreateNewWebViewRequest {
263            servo: self.servo(),
264            responder: AutomaticResponder::new(response_sender, None),
265        };
266        self.delegate().request_create_new(self.clone(), request);
267    }
268
269    pub(crate) fn viewport_details(&self) -> ViewportDetails {
270        // The division by 1 represents the page's default zoom of 100%,
271        // and gives us the appropriate CSSPixel type for the viewport.
272        let inner = self.inner();
273        let viewport_size = inner.rendering_context.size2d().to_f32();
274        let scaled_viewport_size = viewport_size / inner.hidpi_scale_factor;
275        let device_size = self
276            .delegate()
277            .screen_geometry(self.clone())
278            .map(|geometry| geometry.size.to_f32())
279            .unwrap_or_else(|| viewport_size);
280        ViewportDetails {
281            size: scaled_viewport_size / Scale::new(1.0),
282            hidpi_scale_factor: Scale::new(inner.hidpi_scale_factor.0),
283            device_size,
284        }
285    }
286
287    pub(crate) fn from_weak_handle(inner: &Weak<RefCell<WebViewInner>>) -> Option<Self> {
288        inner.upgrade().map(WebView)
289    }
290
291    pub(crate) fn weak_handle(&self) -> Weak<RefCell<WebViewInner>> {
292        Rc::downgrade(&self.0)
293    }
294
295    /// Get the [`WebViewDelegate`] associated with this [`WebView`].
296    pub fn delegate(&self) -> Rc<dyn WebViewDelegate> {
297        self.inner().delegate.clone()
298    }
299
300    /// Get the [`ClipboardDelegate`] associated with this [`WebView`].
301    pub fn clipboard_delegate(&self) -> Rc<dyn ClipboardDelegate> {
302        self.inner().clipboard_delegate.clone()
303    }
304
305    /// Get the [`GamepadDelegate`] associated with this [`WebView`].
306    #[cfg(feature = "gamepad")]
307    pub fn gamepad_delegate(&self) -> Rc<dyn GamepadDelegate> {
308        self.inner().gamepad_delegate.clone()
309    }
310
311    /// Get the [`GeolocationDelegate`] associated with this [`WebView`].
312    pub fn geolocation_delegate(&self) -> Rc<dyn GeolocationDelegate> {
313        self.inner().geolocation_delegate.clone()
314    }
315
316    /// Get the unique identifier for this [`WebView`].
317    pub fn id(&self) -> WebViewId {
318        self.inner().id
319    }
320
321    /// Get the [`RenderingContext`] associated with this [`WebView`].
322    pub fn rendering_context(&self) -> Rc<dyn RenderingContext> {
323        self.inner().rendering_context.clone()
324    }
325
326    /// Get the load status for the page that is currently loading or loaded in this [`WebView`].
327    ///
328    /// The embedder can use [`WebViewDelegate::notify_load_status_changed`] to subscribe
329    /// to changes in the load status.
330    pub fn load_status(&self) -> LoadStatus {
331        self.inner().load_status
332    }
333
334    pub(crate) fn set_load_status(self, new_value: LoadStatus) {
335        if self.inner().load_status == new_value {
336            return;
337        }
338        self.inner_mut().load_status = new_value;
339        self.delegate().notify_load_status_changed(self, new_value);
340    }
341
342    /// Get the URL of the currently active page in this [`WebView`]'s navigation history.
343    /// Returns `None` if no page is currently loaded.
344    pub fn url(&self) -> Option<Url> {
345        let inner = self.inner();
346        inner
347            .back_forward_list
348            .get(inner.back_forward_list_index)
349            .cloned()
350    }
351
352    /// Get the current status text for this [`WebView`]. Returns `None` if there is no status text.
353    ///
354    /// The status text changes as the user interacts with the page, for example, by hovering over
355    /// a link. The embedder can use [`WebViewDelegate::notify_status_text_changed`] to subscribe
356    /// to changes in the status text.
357    pub fn status_text(&self) -> Option<String> {
358        self.inner().status_text.clone()
359    }
360
361    pub(crate) fn set_status_text(self, new_value: Option<String>) {
362        if self.inner().status_text == new_value {
363            return;
364        }
365        self.inner_mut().status_text = new_value.clone();
366        self.delegate().notify_status_text_changed(self, new_value);
367    }
368
369    /// Get the title of the currently active page in this [`WebView`]. Returns `None` if the
370    /// page has no title.
371    ///
372    /// The embedder can use [`WebViewDelegate::notify_page_title_changed`] to subscribe
373    /// to changes in the [`WebView`]'s page title.
374    pub fn page_title(&self) -> Option<String> {
375        self.inner().page_title.clone()
376    }
377
378    pub(crate) fn set_page_title(self, new_value: Option<String>) {
379        if self.inner().page_title == new_value {
380            return;
381        }
382        self.inner_mut().page_title = new_value.clone();
383        self.delegate().notify_page_title_changed(self, new_value);
384    }
385
386    /// Get a read-only reference to the image data for the favicon of the currently
387    /// active page in this [`WebView`]. Returns `None` if no favicon is available
388    /// for the currently active page.
389    ///
390    /// The embedder can use [`WebViewDelegate::notify_favicon_changed`] to subscribe
391    /// to changes in the [`WebView`]'s favicon.
392    pub fn favicon(&self) -> Option<Ref<'_, Image>> {
393        Ref::filter_map(self.inner(), |inner| inner.favicon.as_ref()).ok()
394    }
395
396    pub(crate) fn set_favicon(self, new_value: Option<Image>) {
397        if new_value.is_none() && self.inner().favicon.is_none() {
398            return;
399        }
400        self.inner_mut().favicon = new_value;
401        self.delegate().notify_favicon_changed(self);
402    }
403
404    /// Whether or not this [`WebView`] currently has system focus.
405    pub fn focused(&self) -> bool {
406        self.inner().focused
407    }
408
409    /// Get the current [`Cursor`] for this [`WebView`].
410    ///
411    /// The cursor can change as the user interacts with page content. The embedder
412    /// can use [`WebViewDelegate::notify_cursor_changed`] to subscribe to changes in
413    /// the  [`WebView`]'s cursor.
414    pub fn cursor(&self) -> Cursor {
415        self.inner().cursor
416    }
417
418    pub(crate) fn set_cursor(self, new_value: Cursor) {
419        if self.inner().cursor == new_value {
420            return;
421        }
422        self.inner_mut().cursor = new_value;
423        self.delegate().notify_cursor_changed(self, new_value);
424    }
425
426    /// Notify Servo that this [`WebView`] has gained or lost system focus.
427    ///
428    /// All [`WebView`]s start with system focus activated, so embedders are
429    /// expected to explicitly set this to false when the containing view loses
430    /// focus.
431    pub fn set_focused(&self, focused: bool) {
432        let old_focused = std::mem::replace(&mut self.inner_mut().focused, focused);
433        if old_focused != focused {
434            self.servo().constellation_proxy().send(
435                EmbedderToConstellationMessage::SetWebViewHasSystemFocus(self.id(), focused),
436            );
437        }
438    }
439
440    /// Whether or not this [`WebView`] has animating content, such as a CSS animation or
441    /// transition or is running `requestAnimationFrame` callbacks. This indicates that the
442    /// embedding application should be spinning the Servo event loop on regular intervals
443    /// in order to trigger animation updates.
444    pub fn animating(&self) -> bool {
445        self.inner().animating
446    }
447
448    pub(crate) fn set_animating(self, new_value: bool) {
449        if self.inner().animating == new_value {
450            return;
451        }
452        self.inner_mut().animating = new_value;
453        self.delegate().notify_animating_changed(self, new_value);
454    }
455
456    /// The size of this [`WebView`]'s [`RenderingContext`].
457    pub fn size(&self) -> DeviceSize {
458        self.inner().rendering_context.size2d().to_f32()
459    }
460
461    /// Request that the given [`WebView`]'s [`RenderingContext`] be resized. Note that the
462    /// minimum size for a WebView is 1 pixel by 1 pixel so any requested size will be
463    /// clamped by that value.
464    ///
465    /// This will also resize any other [`WebView`] using the same [`RenderingContext`]. A
466    /// [`WebView`] is always as big as its [`RenderingContext`].
467    pub fn resize(&self, new_size: PhysicalSize<u32>) {
468        let new_size = PhysicalSize {
469            width: new_size.width.max(MINIMUM_WEBVIEW_SIZE.width as u32),
470            height: new_size.height.max(MINIMUM_WEBVIEW_SIZE.height as u32),
471        };
472
473        self.servo()
474            .paint()
475            .resize_rendering_context(self.id(), new_size);
476    }
477
478    /// Get the HiDPI scale factor for this [`WebView`].
479    pub fn hidpi_scale_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel> {
480        self.inner().hidpi_scale_factor
481    }
482
483    /// Set the HiDPI scale factor for this [`WebView`].
484    ///
485    /// This scale factor determines how device-independent pixels map to physical device pixels
486    /// and therefore depends on which device this [`WebView`] is being displayed.
487    pub fn set_hidpi_scale_factor(
488        &self,
489        new_scale_factor: Scale<f32, DeviceIndependentPixel, DevicePixel>,
490    ) {
491        if self.inner().hidpi_scale_factor == new_scale_factor {
492            return;
493        }
494
495        self.inner_mut().hidpi_scale_factor = new_scale_factor;
496        self.servo()
497            .paint()
498            .set_hidpi_scale_factor(self.id(), new_scale_factor);
499    }
500
501    /// Make this [`WebView`] visible within its [`RenderingContext`].
502    pub fn show(&self) {
503        self.servo()
504            .paint()
505            .show_webview(self.id())
506            .expect("BUG: invalid WebView instance");
507    }
508
509    /// Hide this [`WebView`] within its [`RenderingContext`].
510    pub fn hide(&self) {
511        self.servo()
512            .paint()
513            .hide_webview(self.id())
514            .expect("BUG: invalid WebView instance");
515    }
516
517    /// Notify this [`WebView`] of a change to the system theme (e.g. light or dark mode).
518    pub fn notify_theme_change(&self, theme: Theme) {
519        self.servo()
520            .constellation_proxy()
521            .send(EmbedderToConstellationMessage::ThemeChange(
522                self.id(),
523                theme,
524            ))
525    }
526
527    /// Load the given URL into this [`WebView`] using the default request headers.
528    ///
529    /// This pushes a new entry onto the navigation history, so the user can navigate
530    /// back to the previous page.
531    pub fn load(&self, url: Url) {
532        self.servo()
533            .constellation_proxy()
534            .send(EmbedderToConstellationMessage::LoadUrl(
535                self.id(),
536                UrlRequest::new(url),
537            ))
538    }
539
540    /// Load a [`UrlRequest`] with custom headers into this [`WebView`].
541    ///
542    /// This pushes a new entry onto the navigation history, so the user can navigate
543    /// back to the previous page.
544    pub fn load_request(&self, url_request: UrlRequest) {
545        self.servo()
546            .constellation_proxy()
547            .send(EmbedderToConstellationMessage::LoadUrl(
548                self.id(),
549                url_request,
550            ))
551    }
552
553    /// Reload the currently loaded page in this [`WebView`].
554    pub fn reload(&self) {
555        self.inner_mut().load_status = LoadStatus::Started;
556        self.servo()
557            .constellation_proxy()
558            .send(EmbedderToConstellationMessage::Reload(self.id()))
559    }
560
561    /// Whether or not this [`WebView`] can go backward in its navigation history.
562    ///
563    /// This is `false` if the currently active page is the oldest entry in the
564    /// [`WebView`]'s navigation history.
565    pub fn can_go_back(&self) -> bool {
566        self.inner().back_forward_list_index != 0
567    }
568
569    /// Go backward in this [`WebView`]'s navigation history by the given number of steps.
570    ///
571    /// Returns a [`TraversalId`] that can be used with the
572    /// [`WebViewDelegate::notify_traversal_complete`] callback to determine when the
573    /// traversal is complete.
574    pub fn go_back(&self, amount: usize) -> TraversalId {
575        let request = SessionHistoryTraversalRequest::new(
576            self.id(),
577            TraversalDirection::Back(amount),
578            HistoryTraversalSource::Embedder,
579        );
580        let traversal_id = request.id.clone();
581        self.servo()
582            .constellation_proxy()
583            .send(EmbedderToConstellationMessage::TraverseHistory(request));
584        traversal_id
585    }
586
587    /// Whether or not this [`WebView`] can go forward in its navigation history.
588    ///
589    /// This is `false` if the currently active page is the most recent entry in
590    /// the [`WebView`]'s navigation history.
591    pub fn can_go_forward(&self) -> bool {
592        let inner = self.inner();
593        inner.back_forward_list.len() > inner.back_forward_list_index + 1
594    }
595
596    /// Go forward in this [`WebView`]'s navigation history by the given number of steps.
597    ///
598    /// Returns a [`TraversalId`] that can be used with the
599    /// [`WebViewDelegate::notify_traversal_complete`] callback to determine when the
600    /// traversal is complete.
601    pub fn go_forward(&self, amount: usize) -> TraversalId {
602        let request = SessionHistoryTraversalRequest::new(
603            self.id(),
604            TraversalDirection::Forward(amount),
605            HistoryTraversalSource::Embedder,
606        );
607        let traversal_id = request.id.clone();
608        self.servo()
609            .constellation_proxy()
610            .send(EmbedderToConstellationMessage::TraverseHistory(request));
611        traversal_id
612    }
613
614    /// Ask the [`WebView`] to scroll the scrollable area under `point` to the
615    /// given `scroll` destination.
616    pub fn notify_scroll_event(&self, scroll: Scroll, point: WebViewPoint) {
617        self.servo()
618            .paint()
619            .notify_scroll_event(self.id(), scroll, point);
620    }
621
622    /// Notify this [`WebView`] about an [`InputEvent`] such as a mouse click, touch
623    /// event, or key press.
624    ///
625    /// Returns an [`InputEventId`] that can be used with the
626    /// [`WebViewDelegate::notify_input_event_handled`] callback to determine the result of
627    /// processing of the event by the page content.
628    pub fn notify_input_event(&self, event: InputEvent) -> InputEventId {
629        let event: InputEventAndId = event.into();
630        let event_id = event.id;
631        let webview_id = self.id();
632        let servo = &self.inner().servo;
633
634        // Events with a `point` first go to `Paint` for hit testing.
635        if event.event.point().is_some() {
636            if !servo.paint().notify_input_event(self.id(), event) {
637                servo.add_pending_handled_input_event(PendingHandledInputEvent {
638                    event_id,
639                    webview_id,
640                });
641                servo.event_loop_waker().wake();
642            }
643        } else {
644            servo
645                .constellation_proxy()
646                .send(EmbedderToConstellationMessage::ForwardInputEvent(
647                    webview_id, event, None, /* hit_test */
648                ));
649        }
650
651        event_id
652    }
653
654    /// Notify this [`WebView`] about a media session event (e.g. play, pause, next track).
655    pub fn notify_media_session_action_event(&self, event: MediaSessionActionType) {
656        self.servo()
657            .constellation_proxy()
658            .send(EmbedderToConstellationMessage::MediaSessionAction(event));
659    }
660
661    /// Set the page zoom of the [`WebView`]. This sets the final page zoom value of the
662    /// [`WebView`]. Unlike [`WebView::pinch_zoom`] *it is not* multiplied by the current
663    /// page zoom value, but overrides it.
664    ///
665    /// [`WebView`]s have two types of zoom, pinch zoom and page zoom. This adjusts page
666    /// zoom, which will adjust the `devicePixelRatio` of the page and cause it to modify
667    /// its layout.
668    ///
669    /// These values will be clamped internally to the inclusive range [0.1, 10.0]).
670    pub fn set_page_zoom(&self, new_zoom: f32) {
671        self.servo().paint().set_page_zoom(self.id(), new_zoom);
672    }
673
674    /// Get the page zoom of the [`WebView`].
675    pub fn page_zoom(&self) -> f32 {
676        self.servo().paint().page_zoom(self.id())
677    }
678
679    /// Adjust the pinch zoom on this [`WebView`] multiplying the current pinch zoom
680    /// level with the provided `pinch_zoom_delta`.
681    ///
682    /// [`WebView`]s have two types of zoom, pinch zoom and page zoom. This adjusts pinch
683    /// zoom, which is a type of zoom which does not modify layout, and instead simply
684    /// magnifies the view in the viewport.
685    ///
686    /// The final pinch zoom values will be clamped to defaults (the inclusive range [1.0, 10.0]).
687    /// The values used for clamping can be adjusted by page content when `<meta viewport>`
688    /// parsing is enabled via `Prefs::viewport_meta_enabled`, exclusively on mobile devices.
689    pub fn adjust_pinch_zoom(&self, pinch_zoom_delta: f32, center: DevicePoint) {
690        self.servo()
691            .paint()
692            .adjust_pinch_zoom(self.id(), pinch_zoom_delta, center);
693    }
694
695    /// Get the pinch zoom of the [`WebView`].
696    pub fn pinch_zoom(&self) -> f32 {
697        self.servo().paint().pinch_zoom(self.id())
698    }
699
700    /// Get the ratio of physical device pixels to CSS pixels for this [`WebView`].
701    ///
702    /// The returned scale factor takes into account page zoom, pinch zoom and the
703    /// HiDPI scaling factor.
704    pub fn device_pixels_per_css_pixel(&self) -> Scale<f32, CSSPixel, DevicePixel> {
705        self.servo().paint().device_pixels_per_page_pixel(self.id())
706    }
707
708    /// Tell the currently active page in this [`WebView`] to exit fullscreen mode.
709    pub fn exit_fullscreen(&self) {
710        self.servo()
711            .constellation_proxy()
712            .send(EmbedderToConstellationMessage::ExitFullScreen(self.id()));
713    }
714
715    /// Toggle the given [`WebRenderDebugOption`] from its current state.
716    ///
717    /// Note that this method toggles the debugging options globally i.e., it affects
718    /// all [`WebView`]s managed by Servo and not just the [`WebView`] on which
719    /// this method is invoked.
720    pub fn toggle_webrender_debugging(&self, debugging: WebRenderDebugOption) {
721        self.servo().paint().toggle_webrender_debug(debugging);
722    }
723
724    /// Capture the current WebRender state for this [`WebView`] for debugging.
725    ///
726    /// Note that the captured state includes information about all [`WebView`]s
727    /// that share this [`WebView`]'s [`RenderingContext`].
728    pub fn capture_webrender(&self) {
729        self.servo().paint().capture_webrender(self.id());
730    }
731
732    /// Paint the contents of this [`WebView`] into its [`RenderingContext`].
733    pub fn paint(&self) {
734        self.servo().paint().render(self.id());
735    }
736
737    /// Get the [`UserContentManager`] associated with this [`WebView`].
738    pub fn user_content_manager(&self) -> Option<Rc<UserContentManager>> {
739        self.inner().user_content_manager.clone()
740    }
741
742    /// Evaluate the specified string of JavaScript code. Once execution is complete or an error
743    /// occurs, Servo will call `callback`.
744    pub fn evaluate_javascript<T: ToString>(
745        &self,
746        script: T,
747        callback: impl FnOnce(Result<JSValue, JavaScriptEvaluationError>) + 'static,
748    ) {
749        self.servo().javascript_evaluator_mut().evaluate(
750            self.id(),
751            script.to_string(),
752            Box::new(callback),
753        );
754    }
755
756    /// Asynchronously take a screenshot of the [`WebView`] contents, given a `rect` or the whole
757    /// viewport, if no `rect` is given.
758    ///
759    /// This method will wait until the [`WebView`] is ready before the screenshot is taken.
760    /// This includes waiting for:
761    ///
762    ///  - all frames to fire their `load` event.
763    ///  - all render blocking elements, such as stylesheets included via the `<link>`
764    ///    element, to stop blocking the rendering.
765    ///  - all images to be loaded and displayed.
766    ///  - all web fonts are loaded.
767    ///  - the `reftest-wait` and `test-wait` classes have been removed from the root element.
768    ///  - the rendering is up-to-date
769    ///
770    /// Once all these conditions are met and the rendering does not have any pending frames
771    /// to render, the provided `callback` will be called with the results of the screenshot
772    /// operation.
773    pub fn take_screenshot(
774        &self,
775        rect: Option<WebViewRect>,
776        callback: impl FnOnce(Result<RgbaImage, ScreenshotCaptureError>) + 'static,
777    ) {
778        self.servo()
779            .paint()
780            .request_screenshot(self.id(), rect, Box::new(callback));
781    }
782
783    pub(crate) fn set_history(self, new_back_forward_list: Vec<ServoUrl>, new_index: usize) {
784        {
785            let mut inner_mut = self.inner_mut();
786            inner_mut.back_forward_list_index = new_index;
787            inner_mut.back_forward_list = new_back_forward_list
788                .into_iter()
789                .map(ServoUrl::into_url)
790                .collect();
791        }
792
793        let back_forward_list = self.inner().back_forward_list.clone();
794        let back_forward_list_index = self.inner().back_forward_list_index;
795        self.delegate().notify_url_changed(
796            self.clone(),
797            back_forward_list[back_forward_list_index].clone(),
798        );
799        self.delegate().notify_history_changed(
800            self.clone(),
801            back_forward_list,
802            back_forward_list_index,
803        );
804    }
805
806    pub(crate) fn show_embedder_control(
807        self,
808        control_id: EmbedderControlId,
809        position: DeviceIntRect,
810        embedder_control_request: EmbedderControlRequest,
811    ) {
812        let constellation_proxy = self.servo().constellation_proxy().clone();
813        let embedder_control = match embedder_control_request {
814            EmbedderControlRequest::SelectElement(request) => {
815                EmbedderControl::SelectElement(SelectElement {
816                    id: control_id,
817                    select_element_request: request,
818                    position,
819                    constellation_proxy,
820                    response_sent: false,
821                })
822            },
823            EmbedderControlRequest::ColorPicker(current_color) => {
824                EmbedderControl::ColorPicker(ColorPicker {
825                    id: control_id,
826                    current_color: Some(current_color),
827                    position,
828                    constellation_proxy,
829                    response_sent: false,
830                })
831            },
832            EmbedderControlRequest::InputMethod(input_method_request) => {
833                EmbedderControl::InputMethod(InputMethodControl {
834                    id: control_id,
835                    input_method_type: input_method_request.input_method_type,
836                    text: input_method_request.text,
837                    insertion_point: input_method_request.insertion_point,
838                    position,
839                    multiline: input_method_request.multiline,
840                    allow_virtual_keyboard: input_method_request.allow_virtual_keyboard,
841                })
842            },
843            EmbedderControlRequest::ContextMenu(mut context_menu_request) => {
844                for item in context_menu_request.items.iter_mut() {
845                    match item {
846                        ContextMenuItem::Item {
847                            action: ContextMenuAction::GoBack,
848                            enabled,
849                            ..
850                        } => *enabled = self.can_go_back(),
851                        ContextMenuItem::Item {
852                            action: ContextMenuAction::GoForward,
853                            enabled,
854                            ..
855                        } => *enabled = self.can_go_forward(),
856                        _ => {},
857                    }
858                }
859                EmbedderControl::ContextMenu(ContextMenu {
860                    id: control_id,
861                    position,
862                    items: context_menu_request.items,
863                    element_info: context_menu_request.element_info,
864                    constellation_proxy,
865                    response_sent: false,
866                })
867            },
868            EmbedderControlRequest::FilePicker { .. } => {
869                unreachable!("This message should be routed through the FileManagerThread")
870            },
871        };
872
873        self.delegate()
874            .show_embedder_control(self.clone(), embedder_control);
875    }
876
877    /// AccessKit subtree id for this [`WebView`], if accessibility is active.
878    pub fn accesskit_tree_id(&self) -> Option<TreeId> {
879        self.inner().accesskit_tree_id
880    }
881
882    /// Activate or deactivate accessibility features for this [`WebView`], returning the
883    /// AccessKit subtree id if accessibility is now active.
884    ///
885    /// After accessibility is activated, you must [graft] (with [`set_tree_id()`]) the returned
886    /// [`TreeId`] into your application’s main AccessKit tree as soon as possible, *before*
887    /// sending any tree updates from the webview to your AccessKit adapter. Otherwise you may
888    /// violate AccessKit’s subtree invariants and **panic**.
889    ///
890    /// If your impl for [`WebViewDelegate::notify_accessibility_tree_update()`] can’t create the
891    /// graft node (and send *that* update to AccessKit) before sending any updates from this
892    /// webview to AccessKit, then it must queue those updates until it can guarantee that.
893    ///
894    /// [graft]: https://docs.rs/accesskit/0.24.0/accesskit/struct.Node.html#method.tree_id
895    /// [`set_tree_id()`]: https://docs.rs/accesskit/0.24.0/accesskit/struct.Node.html#method.set_tree_id
896    pub fn set_accessibility_active(&self, active: bool) -> Option<TreeId> {
897        if !pref!(accessibility_enabled) {
898            return None;
899        }
900
901        if active == self.inner().accesskit_tree_id.is_some() {
902            return self.accesskit_tree_id();
903        }
904
905        if active {
906            let accesskit_tree_id = TreeId(AccesskitUuid::new_v4());
907            self.inner_mut().accesskit_tree_id = Some(accesskit_tree_id);
908        } else {
909            self.inner_mut().accesskit_tree_id = None;
910            self.inner_mut().grafted_accesskit_tree_id = None;
911            self.inner_mut().grafted_accesskit_tree_epoch = None;
912        }
913
914        self.send_accessibility_root_node();
915
916        self.servo().constellation_proxy().send(
917            EmbedderToConstellationMessage::SetAccessibilityActive(self.id(), active),
918        );
919
920        self.accesskit_tree_id()
921    }
922
923    pub(crate) fn notify_document_accessibility_tree_id(&self, grafted_tree_id: TreeId) {
924        let old_grafted_tree_id = self
925            .inner_mut()
926            .grafted_accesskit_tree_id
927            .replace(grafted_tree_id);
928        // TODO(#4344): try to avoid duplicate notifications in the first place?
929        // (see ConstellationWebView::new for more details)
930        if old_grafted_tree_id == Some(grafted_tree_id) {
931            return;
932        }
933        self.send_accessibility_root_node();
934    }
935
936    /// Record that this [`WebView`]'s accessibility viewport geometry changed (its size, page or
937    /// pinch zoom, or HiDPI scale). The root accessibility node covers the viewport and carries the
938    /// scale the grafted document nodes are resolved against, so it must be re-sent whenever that
939    /// geometry changes. This is called from the paint layer via
940    /// [`WebViewTrait::notify_viewport_updated()`]; the re-send itself is deferred to [`Servo`]'s
941    /// event loop (see `Servo::resend_accessibility_root_nodes_for_viewport_changes()`) so that it
942    /// never calls into the [`WebViewDelegate`] while the paint `RefCell` (or the embedder-facing
943    /// method that triggered the change) is on the stack.
944    pub(crate) fn note_accessibility_viewport_changed(&self) {
945        self.inner().accessibility_viewport_changed.set(true);
946    }
947
948    /// Take (and reset) the flag set by [`Self::note_accessibility_viewport_changed()`].
949    pub(crate) fn take_accessibility_viewport_changed(&self) -> bool {
950        self.inner().accessibility_viewport_changed.take()
951    }
952
953    /// Send the `WebView`-level accessibility root node, which grafts in the current document's
954    /// tree and carries the viewport bounds and scale that the document's own node bounds are
955    /// resolved against.
956    ///
957    /// This must be re-sent whenever that geometry changes, not only when the grafted document
958    /// changes, because the document nodes underneath are expressed relative to it.
959    pub(crate) fn send_accessibility_root_node(&self) {
960        let Some(webview_accesskit_tree_id) = self.inner().accesskit_tree_id else {
961            return;
962        };
963        let Some(grafted_tree_id) = self.inner().grafted_accesskit_tree_id else {
964            return;
965        };
966        let root_node_id = NodeId(0);
967        let mut root_node = AccesskitNode::new(Role::ScrollView);
968        // The root node covers the whole viewport, in the same coordinate space as the bounds of
969        // the document nodes grafted underneath it: CSS pixels relative to the viewport origin.
970        //
971        // Its transform converts those CSS pixels into device pixels. That scale is everything
972        // which maps a CSS pixel to a device pixel — page zoom, pinch zoom and HiDPI scale factor.
973        //
974        // See `update_bounds_from_dom_node()` in
975        // `components/layout/accessibility_tree.rs` for how bounds are computed for web contents.
976
977        // Compute the ratio from the WebView's current settings. `device_pixels_per_css_pixel()`
978        // reports the ratio the latest rendered display list was produced with, which lags behind
979        // the zoom/HiDPI change that triggered this call, since reflow is asynchronous.
980        let device_pixels_per_css_pixel =
981            self.page_zoom() * self.hidpi_scale_factor().get() * self.pinch_zoom();
982        let size =
983            self.size() / Scale::<f32, CSSPixel, DevicePixel>::new(device_pixels_per_css_pixel);
984        root_node.set_bounds(AccesskitRect::new(
985            0.0,
986            0.0,
987            size.width as f64,
988            size.height as f64,
989        ));
990        // AccessKit asks that a node with an identity transform leave it unset.
991        if device_pixels_per_css_pixel != 1.0 {
992            root_node.set_transform(AccesskitAffine::scale(device_pixels_per_css_pixel as f64));
993        }
994        let graft_node_id = NodeId(1);
995        let mut graft_node = AccesskitNode::new(Role::GenericContainer);
996        graft_node.set_tree_id(grafted_tree_id);
997        root_node.set_children(vec![graft_node_id]);
998        self.delegate().notify_accessibility_tree_update(
999            self.clone(),
1000            TreeUpdate {
1001                nodes: vec![(root_node_id, root_node), (graft_node_id, graft_node)],
1002                tree: Some(Tree {
1003                    root: root_node_id,
1004                    toolkit_name: None,
1005                    toolkit_version: None,
1006                }),
1007                tree_id: webview_accesskit_tree_id,
1008                focus: root_node_id,
1009            },
1010        );
1011    }
1012
1013    /// After a new tree has been grafted, move focus to the graft node so that the grafted tree's
1014    /// focused node becomes the focused node for the webview.
1015    fn move_accessibility_focus_to_graft_node(&self) {
1016        let Some(webview_accesskit_tree_id) = self.inner().accesskit_tree_id else {
1017            return;
1018        };
1019        let root_node_id = NodeId(0);
1020        let graft_node_id = NodeId(1);
1021
1022        self.delegate().notify_accessibility_tree_update(
1023            self.clone(),
1024            TreeUpdate {
1025                nodes: vec![],
1026                tree: Some(Tree {
1027                    root: root_node_id,
1028                    toolkit_name: None,
1029                    toolkit_version: None,
1030                }),
1031                tree_id: webview_accesskit_tree_id,
1032                focus: graft_node_id,
1033            },
1034        );
1035    }
1036
1037    pub(crate) fn process_accessibility_tree_update(&self, tree_update: TreeUpdate, epoch: Epoch) {
1038        if self
1039            .inner()
1040            .grafted_accesskit_tree_epoch
1041            .is_some_and(|current| epoch < current)
1042        {
1043            // We expect this to happen occasionally when the constellation navigates, because
1044            // deactivating accessibility happens asynchronously, so the script thread of the
1045            // previously active document may continue sending updates for a short period of time.
1046            debug!("Ignoring stale tree update for {:?}", tree_update.tree_id);
1047            return;
1048        }
1049        if self
1050            .inner()
1051            .grafted_accesskit_tree_epoch
1052            .is_none_or(|current| epoch > current)
1053        {
1054            // This three-step process is necessary because we need the graft node to be the focused
1055            // node in the WebView's accessibility tree, but we can't make it the focused node until
1056            // the graft is completed by sending the initial tree update for the new tree.
1057            self.notify_document_accessibility_tree_id(tree_update.tree_id);
1058            self.inner_mut().grafted_accesskit_tree_epoch = Some(epoch);
1059
1060            self.delegate()
1061                .notify_accessibility_tree_update(self.clone(), tree_update);
1062
1063            self.move_accessibility_focus_to_graft_node();
1064            return;
1065        }
1066
1067        self.delegate()
1068            .notify_accessibility_tree_update(self.clone(), tree_update);
1069    }
1070
1071    /// Clear the session history of this [`WebView`]. The session history is also known
1072    /// as the back-forward cache. Once cleared, [`WebViewDelegate::notify_history`]
1073    /// will be called asynchronously and the resulting session history will contain only
1074    /// a single item with the current URL.
1075    pub fn clear_session_history(&self) {
1076        self.servo().constellation_proxy().send(
1077            EmbedderToConstellationMessage::ClearSessionHistory(self.id()),
1078        );
1079    }
1080}
1081
1082/// A structure used to expose a view of the [`WebView`] to the Servo
1083/// renderer, without having the Servo renderer depend on the embedding layer.
1084struct ServoRendererWebView {
1085    id: WebViewId,
1086    weak_handle: Weak<RefCell<WebViewInner>>,
1087}
1088
1089impl WebViewTrait for ServoRendererWebView {
1090    fn id(&self) -> WebViewId {
1091        self.id
1092    }
1093
1094    fn screen_geometry(&self) -> Option<ScreenGeometry> {
1095        let webview = WebView::from_weak_handle(&self.weak_handle)?;
1096        webview.delegate().screen_geometry(webview)
1097    }
1098
1099    fn set_animating(&self, new_value: bool) {
1100        if let Some(webview) = WebView::from_weak_handle(&self.weak_handle) {
1101            webview.set_animating(new_value);
1102        }
1103    }
1104
1105    fn notify_viewport_updated(&self) {
1106        if let Some(webview) = WebView::from_weak_handle(&self.weak_handle) {
1107            webview.note_accessibility_viewport_changed();
1108        }
1109    }
1110}
1111
1112/// Builder for creating a [`WebView`].
1113pub struct WebViewBuilder {
1114    servo: Servo,
1115    rendering_context: Rc<dyn RenderingContext>,
1116    delegate: Rc<dyn WebViewDelegate>,
1117    url: Option<Url>,
1118    hidpi_scale_factor: Scale<f32, DeviceIndependentPixel, DevicePixel>,
1119    create_new_webview_responder: Option<AutomaticResponder<Option<NewWebViewDetails>>>,
1120    user_content_manager: Option<Rc<UserContentManager>>,
1121    clipboard_delegate: Option<Rc<dyn ClipboardDelegate>>,
1122    #[cfg(feature = "gamepad")]
1123    gamepad_delegate: Option<Rc<dyn GamepadDelegate>>,
1124    geolocation_delegate: Option<Rc<dyn GeolocationDelegate>>,
1125}
1126
1127impl WebViewBuilder {
1128    /// Create a [`WebViewBuilder`] that can be used to configure and create a [`WebView`].
1129    ///
1130    /// The new [`WebView`] will be managed by the given `servo` instance and will
1131    /// use `rendering_context` to paint its contents.
1132    pub fn new(servo: &Servo, rendering_context: Rc<dyn RenderingContext>) -> Self {
1133        Self {
1134            servo: servo.clone(),
1135            rendering_context,
1136            url: None,
1137            hidpi_scale_factor: Scale::new(1.0),
1138            delegate: Rc::new(DefaultWebViewDelegate),
1139            create_new_webview_responder: None,
1140            user_content_manager: None,
1141            clipboard_delegate: None,
1142            #[cfg(feature = "gamepad")]
1143            gamepad_delegate: None,
1144            geolocation_delegate: None,
1145        }
1146    }
1147
1148    pub(crate) fn new_for_create_request(
1149        servo: &Servo,
1150        rendering_context: Rc<dyn RenderingContext>,
1151        responder: AutomaticResponder<Option<NewWebViewDetails>>,
1152    ) -> Self {
1153        let mut builder = Self::new(servo, rendering_context);
1154        builder.create_new_webview_responder = Some(responder);
1155        builder
1156    }
1157
1158    /// Set the [`WebViewDelegate`] that will receive notifications about the events
1159    /// in the [`WebView`] being created.
1160    pub fn delegate(mut self, delegate: Rc<dyn WebViewDelegate>) -> Self {
1161        self.delegate = delegate;
1162        self
1163    }
1164
1165    /// Set the initial URL to load in the [`WebView`] being created.
1166    pub fn url(mut self, url: Url) -> Self {
1167        self.url = Some(url);
1168        self
1169    }
1170
1171    /// Set the initial HiDPI scale factor for the [`WebView`] being created.
1172    pub fn hidpi_scale_factor(
1173        mut self,
1174        hidpi_scale_factor: Scale<f32, DeviceIndependentPixel, DevicePixel>,
1175    ) -> Self {
1176        self.hidpi_scale_factor = hidpi_scale_factor;
1177        self
1178    }
1179
1180    /// Set the [`UserContentManager`] for the `WebView` being created. The same
1181    /// `UserContentManager` can be shared among multiple `WebView`s. Any updates
1182    /// to the `UserContentManager` will take effect only after the document is reloaded.
1183    pub fn user_content_manager(mut self, user_content_manager: Rc<UserContentManager>) -> Self {
1184        self.user_content_manager = Some(user_content_manager);
1185        self
1186    }
1187
1188    /// Set the [`ClipboardDelegate`] for the `WebView` being created. The same
1189    /// [`ClipboardDelegate`] can be shared among multiple `WebView`s.
1190    pub fn clipboard_delegate(mut self, clipboard_delegate: Rc<dyn ClipboardDelegate>) -> Self {
1191        self.clipboard_delegate = Some(clipboard_delegate);
1192        self
1193    }
1194
1195    /// Set the [`GamepadDelegate`] for the `WebView` being created. The same
1196    /// [`GamepadDelegate`] can be shared among multiple `WebView`s.
1197    #[cfg(feature = "gamepad")]
1198    pub fn gamepad_delegate(mut self, gamepad_delegate: Rc<dyn GamepadDelegate>) -> Self {
1199        self.gamepad_delegate = Some(gamepad_delegate);
1200        self
1201    }
1202
1203    /// Set the [`GeolocationDelegate`] for the `WebView` being created. The same
1204    /// [`GeolocationDelegate`] can be shared among multiple `WebView`s.
1205    pub fn geolocation_delegate(
1206        mut self,
1207        geolocation_delegate: Rc<dyn GeolocationDelegate>,
1208    ) -> Self {
1209        self.geolocation_delegate = Some(geolocation_delegate);
1210        self
1211    }
1212
1213    /// Create the [`WebView`] using the configuration specified in this [`WebViewBuilder`].
1214    pub fn build(self) -> WebView {
1215        WebView::new(self)
1216    }
1217}