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