Skip to main content

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