servoshell/
running_app_state.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
5//! Shared state and methods for desktop and EGL implementations.
6
7use std::cell::{Cell, Ref, RefCell};
8use std::collections::HashMap;
9use std::collections::hash_map::Entry;
10use std::rc::Rc;
11
12use crossbeam_channel::{Receiver, Sender, unbounded};
13use euclid::Rect;
14use image::{DynamicImage, ImageFormat, RgbaImage};
15use log::{error, info, warn};
16use servo::{
17    AllowOrDenyRequest, AuthenticationRequest, CSSPixel, DeviceIntPoint, DeviceIntSize,
18    EmbedderControl, EmbedderControlId, EventLoopWaker, GamepadHapticEffectType, GenericSender,
19    InputEvent, InputEventId, InputEventResult, IpcSender, JSValue, LoadStatus, MediaSessionEvent,
20    PermissionRequest, PrefValue, ScreenshotCaptureError, Servo, ServoDelegate, ServoError,
21    TraversalId, WebDriverCommandMsg, WebDriverJSResult, WebDriverLoadStatus,
22    WebDriverScriptCommand, WebDriverSenders, WebView, WebViewBuilder, WebViewDelegate, WebViewId,
23    pref,
24};
25use url::Url;
26
27use crate::GamepadSupport;
28use crate::prefs::{EXPERIMENTAL_PREFS, ServoShellPreferences};
29use crate::webdriver::WebDriverEmbedderControls;
30use crate::window::{PlatformWindow, ServoShellWindow, ServoShellWindowId};
31
32#[derive(Default)]
33pub struct WebViewCollection {
34    /// List of top-level browsing contexts.
35    /// Modified by EmbedderMsg::WebViewOpened and EmbedderMsg::WebViewClosed,
36    /// and we exit if it ever becomes empty.
37    webviews: HashMap<WebViewId, WebView>,
38
39    /// The order in which the webviews were created.
40    pub(crate) creation_order: Vec<WebViewId>,
41
42    /// The [`WebView`] that is currently active. This is the [`WebView`] that is shown and has
43    /// input focus.
44    active_webview_id: Option<WebViewId>,
45}
46
47impl WebViewCollection {
48    pub fn add(&mut self, webview: WebView) {
49        let id = webview.id();
50        self.creation_order.push(id);
51        self.webviews.insert(id, webview);
52    }
53
54    /// Removes a webview from the collection by [`WebViewId`]. If the removed [`WebView`] was the active
55    /// [`WebView`] then the next newest [`WebView`] will be activated.
56    pub fn remove(&mut self, id: WebViewId) -> Option<WebView> {
57        self.creation_order.retain(|&webview_id| webview_id != id);
58        let removed_webview = self.webviews.remove(&id);
59
60        if self.active_webview_id == Some(id) {
61            self.active_webview_id = None;
62            if let Some(newest) = self.creation_order.last() {
63                self.activate_webview(*newest);
64            }
65        }
66
67        removed_webview
68    }
69
70    pub fn get(&self, id: WebViewId) -> Option<&WebView> {
71        self.webviews.get(&id)
72    }
73
74    pub fn contains(&self, id: WebViewId) -> bool {
75        self.webviews.contains_key(&id)
76    }
77
78    pub fn active(&self) -> Option<&WebView> {
79        self.active_webview_id.and_then(|id| self.webviews.get(&id))
80    }
81
82    pub fn active_id(&self) -> Option<WebViewId> {
83        self.active_webview_id
84    }
85
86    /// Gets a reference to the most recently created webview, if any.
87    pub fn newest(&self) -> Option<&WebView> {
88        self.creation_order
89            .last()
90            .and_then(|id| self.webviews.get(id))
91    }
92
93    pub fn all_in_creation_order(&self) -> impl Iterator<Item = (WebViewId, &WebView)> {
94        self.creation_order
95            .iter()
96            .filter_map(move |id| self.webviews.get(id).map(|webview| (*id, webview)))
97    }
98
99    /// Returns an iterator over all webview references (in arbitrary order).
100    pub fn values(&self) -> impl Iterator<Item = &WebView> {
101        self.webviews.values()
102    }
103
104    /// Returns true if the collection contains no webviews.
105    pub fn is_empty(&self) -> bool {
106        self.webviews.is_empty()
107    }
108
109    pub(crate) fn activate_webview(&mut self, id_to_activate: WebViewId) {
110        assert!(self.creation_order.contains(&id_to_activate));
111
112        self.active_webview_id = Some(id_to_activate);
113        for (webview_id, webview) in self.all_in_creation_order() {
114            if id_to_activate == webview_id {
115                webview.show();
116                webview.focus();
117            } else {
118                webview.hide();
119                webview.blur();
120            }
121        }
122    }
123
124    pub(crate) fn activate_webview_by_index(&mut self, index: usize) {
125        self.activate_webview(
126            *self
127                .creation_order
128                .get(index)
129                .expect("Tried to activate an unknown WebView"),
130        );
131    }
132}
133
134/// A command received via the user interacting with the user interface.
135#[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
136pub(crate) enum UserInterfaceCommand {
137    Go(String),
138    Back,
139    Forward,
140    Reload,
141    ReloadAll,
142    NewWebView,
143    CloseWebView(WebViewId),
144    NewWindow,
145}
146
147pub(crate) struct RunningAppState {
148    /// Gamepad support, which may be `None` if it failed to initialize.
149    gamepad_support: RefCell<Option<GamepadSupport>>,
150
151    /// The [`WebDriverSenders`] used to reply to pending WebDriver requests.
152    pub(crate) webdriver_senders: RefCell<WebDriverSenders>,
153
154    /// When running in WebDriver mode, [`WebDriverEmbedderControls`] is a virtual container
155    /// for all embedder controls. This overrides the normal behavior where these controls
156    /// are shown in the GUI or not processed at all in headless mode.
157    pub(crate) webdriver_embedder_controls: WebDriverEmbedderControls,
158
159    /// A [`HashMap`] of pending WebDriver events. It is the WebDriver embedder's responsibility
160    /// to inform the WebDriver server when the event has been fully handled. This map is used
161    /// to report back to WebDriver when that happens.
162    pub(crate) pending_webdriver_events: RefCell<HashMap<InputEventId, Sender<()>>>,
163
164    /// A [`Receiver`] for receiving commands from a running WebDriver server, if WebDriver
165    /// was enabled.
166    pub(crate) webdriver_receiver: Option<Receiver<WebDriverCommandMsg>>,
167
168    /// servoshell specific preferences created during startup of the application.
169    pub(crate) servoshell_preferences: ServoShellPreferences,
170
171    /// A handle to the Servo instance.
172    pub(crate) servo: Servo,
173
174    /// Whether or not the application has achieved stable image output. This is used
175    /// for the `exit_after_stable_image` option.
176    pub(crate) achieved_stable_image: Rc<Cell<bool>>,
177
178    /// Whether or not program exit has been triggered. This means that all windows
179    /// will be destroyed and shutdown will start at the end of the current event loop.
180    exit_scheduled: Cell<bool>,
181
182    /// Whether the user has enabled experimental preferences.
183    experimental_preferences_enabled: Cell<bool>,
184
185    /// The set of [`ServoShellWindow`]s that currently exist for this instance of servoshell.
186    // This is the last field of the struct to ensure that windows are dropped *after* all
187    // other references to the relevant rendering contexts have been destroyed.
188    // See https://github.com/servo/servo/issues/36711.
189    windows: RefCell<HashMap<ServoShellWindowId, Rc<ServoShellWindow>>>,
190}
191
192impl RunningAppState {
193    pub(crate) fn new(
194        servo: Servo,
195        servoshell_preferences: ServoShellPreferences,
196        event_loop_waker: Box<dyn EventLoopWaker>,
197    ) -> Self {
198        servo.set_delegate(Rc::new(ServoShellServoDelegate));
199
200        let gamepad_support = if pref!(dom_gamepad_enabled) {
201            GamepadSupport::maybe_new()
202        } else {
203            None
204        };
205
206        let webdriver_receiver = servoshell_preferences.webdriver_port.get().map(|port| {
207            let (embedder_sender, embedder_receiver) = unbounded();
208            webdriver_server::start_server(port, embedder_sender, event_loop_waker);
209            embedder_receiver
210        });
211
212        let experimental_preferences_enabled =
213            Cell::new(servoshell_preferences.experimental_preferences_enabled);
214
215        Self {
216            windows: Default::default(),
217            gamepad_support: RefCell::new(gamepad_support),
218            webdriver_senders: RefCell::default(),
219            webdriver_embedder_controls: Default::default(),
220            pending_webdriver_events: Default::default(),
221            webdriver_receiver,
222            servoshell_preferences,
223            servo,
224            achieved_stable_image: Default::default(),
225            exit_scheduled: Default::default(),
226            experimental_preferences_enabled,
227        }
228    }
229
230    pub(crate) fn open_window(
231        self: &Rc<Self>,
232        platform_window: Rc<dyn PlatformWindow>,
233        initial_url: Url,
234    ) {
235        let window = Rc::new(ServoShellWindow::new(platform_window));
236        window.create_and_activate_toplevel_webview(self.clone(), initial_url);
237        self.windows.borrow_mut().insert(window.id(), window);
238    }
239
240    pub(crate) fn windows<'a>(
241        &'a self,
242    ) -> Ref<'a, HashMap<ServoShellWindowId, Rc<ServoShellWindow>>> {
243        self.windows.borrow()
244    }
245
246    /// Get any [`ServoShellWindow`] from this state's collection of windows. This is used for
247    /// WebDriver, which currently doesn't have great support for per-window operation.
248    pub(crate) fn any_window(&self) -> Rc<ServoShellWindow> {
249        self.windows
250            .borrow()
251            .values()
252            .next()
253            .expect("Should always have at least one window open when running WebDriver")
254            .clone()
255    }
256
257    pub(crate) fn focused_window(&self) -> Option<Rc<ServoShellWindow>> {
258        self.windows
259            .borrow()
260            .values()
261            .find(|window| window.focused())
262            .cloned()
263    }
264
265    #[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
266    pub(crate) fn window(&self, id: ServoShellWindowId) -> Option<Rc<ServoShellWindow>> {
267        self.windows.borrow().get(&id).cloned()
268    }
269
270    pub(crate) fn webview_by_id(&self, webview_id: WebViewId) -> Option<WebView> {
271        self.maybe_window_for_webview_id(webview_id)?
272            .webview_by_id(webview_id)
273    }
274
275    pub(crate) fn webdriver_receiver(&self) -> Option<&Receiver<WebDriverCommandMsg>> {
276        self.webdriver_receiver.as_ref()
277    }
278
279    pub(crate) fn servo(&self) -> &Servo {
280        &self.servo
281    }
282
283    pub(crate) fn schedule_exit(&self) {
284        // When explicitly required to shutdown, unset webdriver port
285        // which allows normal shutdown.
286        // Note that when not explicitly required to shutdown, we still keep Servo alive
287        // when all tabs are closed when `webdriver_port` enabled, which is necessary
288        // to run wpt test using servodriver.
289        self.servoshell_preferences.webdriver_port.set(None);
290        self.exit_scheduled.set(true);
291    }
292
293    #[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
294    pub(crate) fn experimental_preferences_enabled(&self) -> bool {
295        self.experimental_preferences_enabled.get()
296    }
297
298    #[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
299    pub(crate) fn set_experimental_preferences_enabled(&self, new_value: bool) {
300        let old_value = self.experimental_preferences_enabled.replace(new_value);
301        if old_value == new_value {
302            return;
303        }
304        for pref in EXPERIMENTAL_PREFS {
305            self.servo.set_preference(pref, PrefValue::Bool(new_value));
306        }
307    }
308
309    /// Spins the internal application event loop.
310    ///
311    /// - Notifies Servo about incoming gamepad events
312    /// - Spin the Servo event loop, which will run the compositor and trigger delegate methods.
313    ///
314    /// Returns true if the event loop should continue spinning and false if it should exit.
315    pub(crate) fn spin_event_loop(self: &Rc<Self>) -> bool {
316        self.handle_webdriver_messages();
317
318        if pref!(dom_gamepad_enabled) {
319            self.handle_gamepad_events();
320        }
321
322        self.servo.spin_event_loop();
323
324        for window in self.windows.borrow().values() {
325            window.update_and_request_repaint_if_necessary(self);
326        }
327
328        if self.servoshell_preferences.exit_after_stable_image && self.achieved_stable_image.get() {
329            self.schedule_exit();
330        }
331
332        // When a ServoShellWindow has no more WebViews, close it. When no more windows are open, exit
333        // the application. Do not do this when running WebDriver, which expects to keep running with
334        // no WebView open.
335        if self.servoshell_preferences.webdriver_port.get().is_none() {
336            self.windows
337                .borrow_mut()
338                .retain(|_, window| !self.exit_scheduled.get() && !window.should_close());
339            if self.windows.borrow().is_empty() {
340                self.schedule_exit()
341            }
342        }
343
344        !self.exit_scheduled.get()
345    }
346
347    #[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
348    pub(crate) fn foreach_window_and_interface_commands(
349        self: &Rc<Self>,
350        callback: impl Fn(&ServoShellWindow, Vec<UserInterfaceCommand>),
351    ) {
352        // We clone here to avoid a double borrow. User interface commands can update the list of windows.
353        let windows: Vec<_> = self.windows.borrow().values().cloned().collect();
354        for window in windows {
355            callback(
356                &window,
357                window.platform_window().take_user_interface_commands(),
358            )
359        }
360    }
361
362    pub(crate) fn maybe_window_for_webview_id(
363        &self,
364        webview_id: WebViewId,
365    ) -> Option<Rc<ServoShellWindow>> {
366        for window in self.windows.borrow().values() {
367            if window.contains_webview(webview_id) {
368                return Some(window.clone());
369            }
370        }
371        None
372    }
373
374    pub(crate) fn window_for_webview_id(&self, webview_id: WebViewId) -> Rc<ServoShellWindow> {
375        self.maybe_window_for_webview_id(webview_id)
376            .expect("Looking for unexpected WebView: {webview_id:?}")
377    }
378
379    pub(crate) fn platform_window_for_webview_id(
380        &self,
381        webview_id: WebViewId,
382    ) -> Rc<dyn PlatformWindow> {
383        self.window_for_webview_id(webview_id).platform_window()
384    }
385
386    /// If we are exiting after achieving a stable image or we want to save the display of the
387    /// [`WebView`] to an image file, request a screenshot of the [`WebView`].
388    fn maybe_request_screenshot(&self, webview: WebView) {
389        let output_path = self.servoshell_preferences.output_image_path.clone();
390        if !self.servoshell_preferences.exit_after_stable_image && output_path.is_none() {
391            return;
392        }
393
394        // Never request more than a single screenshot for now.
395        let achieved_stable_image = self.achieved_stable_image.clone();
396        if achieved_stable_image.get() {
397            return;
398        }
399
400        webview.take_screenshot(None, move |image| {
401            achieved_stable_image.set(true);
402
403            let Some(output_path) = output_path else {
404                return;
405            };
406
407            let image = match image {
408                Ok(image) => image,
409                Err(error) => {
410                    error!("Could not take screenshot: {error:?}");
411                    return;
412                },
413            };
414
415            let image_format = ImageFormat::from_path(&output_path).unwrap_or(ImageFormat::Png);
416            if let Err(error) =
417                DynamicImage::ImageRgba8(image).save_with_format(output_path, image_format)
418            {
419                error!("Failed to save screenshot: {error}.");
420            }
421        });
422    }
423
424    pub(crate) fn set_pending_traversal(
425        &self,
426        traversal_id: TraversalId,
427        sender: GenericSender<WebDriverLoadStatus>,
428    ) {
429        self.webdriver_senders
430            .borrow_mut()
431            .pending_traversals
432            .insert(traversal_id, sender);
433    }
434
435    pub(crate) fn set_load_status_sender(
436        &self,
437        webview_id: WebViewId,
438        sender: GenericSender<WebDriverLoadStatus>,
439    ) {
440        self.webdriver_senders
441            .borrow_mut()
442            .load_status_senders
443            .insert(webview_id, sender);
444    }
445
446    fn remove_load_status_sender(&self, webview_id: WebViewId) {
447        self.webdriver_senders
448            .borrow_mut()
449            .load_status_senders
450            .remove(&webview_id);
451    }
452
453    fn set_script_command_interrupt_sender(&self, sender: Option<IpcSender<WebDriverJSResult>>) {
454        self.webdriver_senders
455            .borrow_mut()
456            .script_evaluation_interrupt_sender = sender;
457    }
458
459    pub(crate) fn handle_webdriver_input_event(
460        &self,
461        webview_id: WebViewId,
462        input_event: InputEvent,
463        response_sender: Option<Sender<()>>,
464    ) {
465        if let Some(webview) = self.webview_by_id(webview_id) {
466            let event_id = webview.notify_input_event(input_event);
467            if let Some(response_sender) = response_sender {
468                self.pending_webdriver_events
469                    .borrow_mut()
470                    .insert(event_id, response_sender);
471            }
472        } else {
473            error!("Could not find WebView ({webview_id:?}) for WebDriver event: {input_event:?}");
474        };
475    }
476
477    pub(crate) fn handle_webdriver_screenshot(
478        &self,
479        webview_id: WebViewId,
480        rect: Option<Rect<f32, CSSPixel>>,
481        result_sender: Sender<Result<RgbaImage, ScreenshotCaptureError>>,
482    ) {
483        if let Some(webview) = self.webview_by_id(webview_id) {
484            let rect = rect.map(|rect| rect.to_box2d().into());
485            webview.take_screenshot(rect, move |result| {
486                if let Err(error) = result_sender.send(result) {
487                    warn!("Failed to send response to TakeScreenshot: {error}");
488                }
489            });
490        } else if let Err(error) =
491            result_sender.send(Err(ScreenshotCaptureError::WebViewDoesNotExist))
492        {
493            error!("Failed to send response to TakeScreenshot: {error}");
494        }
495    }
496
497    pub(crate) fn handle_webdriver_script_command(&self, script_command: &WebDriverScriptCommand) {
498        match script_command {
499            WebDriverScriptCommand::ExecuteScript(_webview_id, response_sender) |
500            WebDriverScriptCommand::ExecuteAsyncScript(_webview_id, response_sender) => {
501                // Give embedder a chance to interrupt the script command.
502                // Webdriver only handles 1 script command at a time, so we can
503                // safely set a new interrupt sender and remove the previous one here.
504                self.set_script_command_interrupt_sender(Some(response_sender.clone()));
505            },
506            WebDriverScriptCommand::AddLoadStatusSender(webview_id, load_status_sender) => {
507                self.set_load_status_sender(*webview_id, load_status_sender.clone());
508            },
509            WebDriverScriptCommand::RemoveLoadStatusSender(webview_id) => {
510                self.remove_load_status_sender(*webview_id);
511            },
512            _ => {
513                self.set_script_command_interrupt_sender(None);
514            },
515        }
516    }
517
518    pub(crate) fn handle_webdriver_load_url(
519        &self,
520        webview_id: WebViewId,
521        url: Url,
522        load_status_sender: GenericSender<WebDriverLoadStatus>,
523    ) {
524        let Some(webview) = self.webview_by_id(webview_id) else {
525            return;
526        };
527
528        self.platform_window_for_webview_id(webview_id)
529            .dismiss_embedder_controls_for_webview(webview_id);
530
531        info!("Loading URL in webview {}: {}", webview_id, url);
532        self.set_load_status_sender(webview_id, load_status_sender);
533        webview.load(url);
534    }
535
536    pub(crate) fn handle_gamepad_events(&self) {
537        let Some(active_webview) = self
538            .focused_window()
539            .and_then(|window| window.active_webview())
540        else {
541            return;
542        };
543        if let Some(gamepad_support) = self.gamepad_support.borrow_mut().as_mut() {
544            gamepad_support.handle_gamepad_events(active_webview);
545        }
546    }
547
548    /// Interrupt any ongoing WebDriver-based script evaluation.
549    ///
550    /// From <https://w3c.github.io/webdriver/#dfn-execute-a-function-body>:
551    /// > The rules to execute a function body are as follows. The algorithm returns
552    /// > an ECMAScript completion record.
553    /// >
554    /// > If at any point during the algorithm a user prompt appears, immediately return
555    /// > Completion { Type: normal, Value: null, Target: empty }, but continue to run the
556    /// >  other steps of this algorithm in parallel.
557    fn interrupt_webdriver_script_evaluation(&self) {
558        if let Some(sender) = &self
559            .webdriver_senders
560            .borrow()
561            .script_evaluation_interrupt_sender
562        {
563            sender.send(Ok(JSValue::Null)).unwrap_or_else(|err| {
564                info!(
565                    "Notify dialog appear failed. Maybe the channel to webdriver is closed: {err}"
566                );
567            });
568        }
569    }
570}
571
572impl WebViewDelegate for RunningAppState {
573    fn screen_geometry(&self, webview: WebView) -> Option<servo::ScreenGeometry> {
574        Some(
575            self.platform_window_for_webview_id(webview.id())
576                .screen_geometry(),
577        )
578    }
579
580    fn notify_status_text_changed(&self, webview: WebView, _status: Option<String>) {
581        self.window_for_webview_id(webview.id()).set_needs_update();
582    }
583
584    fn notify_history_changed(&self, webview: WebView, _entries: Vec<Url>, _current: usize) {
585        self.window_for_webview_id(webview.id()).set_needs_update();
586    }
587
588    fn notify_page_title_changed(&self, webview: WebView, _: Option<String>) {
589        self.window_for_webview_id(webview.id()).set_needs_update();
590    }
591
592    fn notify_traversal_complete(&self, _webview: WebView, traversal_id: TraversalId) {
593        let mut webdriver_state = self.webdriver_senders.borrow_mut();
594        if let Entry::Occupied(entry) = webdriver_state.pending_traversals.entry(traversal_id) {
595            let sender = entry.remove();
596            let _ = sender.send(WebDriverLoadStatus::Complete);
597        }
598    }
599
600    fn request_move_to(&self, webview: WebView, new_position: DeviceIntPoint) {
601        self.platform_window_for_webview_id(webview.id())
602            .set_position(new_position);
603    }
604
605    fn request_resize_to(&self, webview: WebView, requested_outer_size: DeviceIntSize) {
606        self.platform_window_for_webview_id(webview.id())
607            .request_resize(&webview, requested_outer_size);
608    }
609
610    fn request_authentication(
611        &self,
612        webview: WebView,
613        authentication_request: AuthenticationRequest,
614    ) {
615        self.platform_window_for_webview_id(webview.id())
616            .show_http_authentication_dialog(webview.id(), authentication_request);
617    }
618
619    fn request_open_auxiliary_webview(&self, parent_webview: WebView) -> Option<WebView> {
620        let window = self.window_for_webview_id(parent_webview.id());
621        let platform_window = window.platform_window();
622
623        let webview =
624            WebViewBuilder::new_auxiliary(&self.servo, platform_window.rendering_context())
625                .hidpi_scale_factor(platform_window.hidpi_scale_factor())
626                .delegate(parent_webview.delegate())
627                .build();
628
629        webview.notify_theme_change(platform_window.theme());
630
631        window.add_webview(webview.clone());
632
633        // When WebDriver is enabled, do not focus and raise the WebView to the top,
634        // as that is what the specification expects. Otherwise, we would like `window.open()`
635        // to create a new foreground tab
636        if self.servoshell_preferences.webdriver_port.get().is_none() {
637            window.activate_webview(webview.id());
638        } else {
639            webview.hide();
640        }
641
642        Some(webview)
643    }
644
645    fn notify_closed(&self, webview: WebView) {
646        self.window_for_webview_id(webview.id())
647            .close_webview(webview.id())
648    }
649
650    fn notify_input_event_handled(
651        &self,
652        webview: WebView,
653        id: InputEventId,
654        result: InputEventResult,
655    ) {
656        self.platform_window_for_webview_id(webview.id())
657            .notify_input_event_handled(&webview, id, result);
658        if let Some(response_sender) = self.pending_webdriver_events.borrow_mut().remove(&id) {
659            let _ = response_sender.send(());
660        }
661    }
662
663    fn notify_cursor_changed(&self, webview: WebView, cursor: servo::Cursor) {
664        self.platform_window_for_webview_id(webview.id())
665            .set_cursor(cursor);
666    }
667
668    fn notify_load_status_changed(&self, webview: WebView, status: LoadStatus) {
669        self.window_for_webview_id(webview.id()).set_needs_update();
670
671        if status == LoadStatus::Complete {
672            if let Some(sender) = self
673                .webdriver_senders
674                .borrow_mut()
675                .load_status_senders
676                .remove(&webview.id())
677            {
678                let _ = sender.send(WebDriverLoadStatus::Complete);
679            }
680            self.maybe_request_screenshot(webview);
681        }
682    }
683
684    fn notify_fullscreen_state_changed(&self, webview: WebView, fullscreen_state: bool) {
685        self.platform_window_for_webview_id(webview.id())
686            .set_fullscreen(fullscreen_state);
687    }
688
689    fn show_bluetooth_device_dialog(
690        &self,
691        webview: WebView,
692        devices: Vec<String>,
693        response_sender: GenericSender<Option<String>>,
694    ) {
695        self.platform_window_for_webview_id(webview.id())
696            .show_bluetooth_device_dialog(webview.id(), devices, response_sender);
697    }
698
699    fn request_permission(&self, webview: WebView, permission_request: PermissionRequest) {
700        self.platform_window_for_webview_id(webview.id())
701            .show_permission_dialog(webview.id(), permission_request);
702    }
703
704    fn notify_new_frame_ready(&self, webview: WebView) {
705        self.window_for_webview_id(webview.id()).set_needs_repaint();
706    }
707
708    fn play_gamepad_haptic_effect(
709        &self,
710        _webview: WebView,
711        index: usize,
712        effect_type: GamepadHapticEffectType,
713        effect_complete_sender: IpcSender<bool>,
714    ) {
715        match self.gamepad_support.borrow_mut().as_mut() {
716            Some(gamepad_support) => {
717                gamepad_support.play_haptic_effect(index, effect_type, effect_complete_sender);
718            },
719            None => {
720                let _ = effect_complete_sender.send(false);
721            },
722        }
723    }
724
725    fn stop_gamepad_haptic_effect(
726        &self,
727        _webview: WebView,
728        index: usize,
729        haptic_stop_sender: IpcSender<bool>,
730    ) {
731        let stopped = match self.gamepad_support.borrow_mut().as_mut() {
732            Some(gamepad_support) => gamepad_support.stop_haptic_effect(index),
733            None => false,
734        };
735        let _ = haptic_stop_sender.send(stopped);
736    }
737
738    fn show_embedder_control(&self, webview: WebView, embedder_control: EmbedderControl) {
739        if self.servoshell_preferences.webdriver_port.get().is_some() {
740            if matches!(&embedder_control, EmbedderControl::SimpleDialog(..)) {
741                self.interrupt_webdriver_script_evaluation();
742
743                // Dialogs block the page load, so need need to notify WebDriver
744                if let Some(sender) = self
745                    .webdriver_senders
746                    .borrow_mut()
747                    .load_status_senders
748                    .get(&webview.id())
749                {
750                    let _ = sender.send(WebDriverLoadStatus::Blocked);
751                };
752            }
753
754            self.webdriver_embedder_controls
755                .show_embedder_control(webview.id(), embedder_control);
756            return;
757        }
758
759        self.window_for_webview_id(webview.id())
760            .show_embedder_control(webview, embedder_control);
761    }
762
763    fn hide_embedder_control(&self, webview: WebView, embedder_control_id: EmbedderControlId) {
764        if self.servoshell_preferences.webdriver_port.get().is_some() {
765            self.webdriver_embedder_controls
766                .hide_embedder_control(webview.id(), embedder_control_id);
767            return;
768        }
769
770        self.window_for_webview_id(webview.id())
771            .hide_embedder_control(webview, embedder_control_id);
772    }
773
774    fn notify_favicon_changed(&self, webview: WebView) {
775        self.window_for_webview_id(webview.id())
776            .notify_favicon_changed(webview);
777    }
778
779    fn notify_media_session_event(&self, webview: WebView, event: MediaSessionEvent) {
780        self.platform_window_for_webview_id(webview.id())
781            .notify_media_session_event(event);
782    }
783
784    fn notify_crashed(&self, webview: WebView, reason: String, backtrace: Option<String>) {
785        self.platform_window_for_webview_id(webview.id())
786            .notify_crashed(webview, reason, backtrace);
787    }
788}
789
790struct ServoShellServoDelegate;
791impl ServoDelegate for ServoShellServoDelegate {
792    fn notify_devtools_server_started(&self, port: u16, _token: String) {
793        info!("Devtools Server running on port {port}");
794    }
795
796    fn request_devtools_connection(&self, request: AllowOrDenyRequest) {
797        request.allow();
798    }
799
800    fn notify_error(&self, error: ServoError) {
801        error!("Saw Servo error: {error:?}!");
802    }
803}