Skip to main content

servoshell/
window.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::{Cell, RefCell};
6use std::rc::Rc;
7use std::sync::atomic::AtomicU64;
8
9use euclid::Scale;
10use log::warn;
11use servo::{
12    AuthenticationRequest, BluetoothDeviceSelectionRequest, ConsoleLogLevel, Cursor,
13    DeviceIndependentIntRect, DeviceIndependentPixel, DeviceIntPoint, DeviceIntSize, DevicePixel,
14    EmbedderControl, EmbedderControlId, InputEventId, InputEventResult, MediaSessionEvent,
15    PermissionRequest, RenderingContext, ScreenGeometry, WebView, WebViewBuilder, WebViewId,
16};
17use url::Url;
18
19use crate::parser::location_bar_input_to_url;
20use crate::running_app_state::{RunningAppState, UserInterfaceCommand, WebViewCollection};
21
22// This should vary by zoom level and maybe actual text size (focused or under cursor)
23#[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
24pub(crate) const LINE_HEIGHT: f32 = 76.0;
25#[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
26pub(crate) const LINE_WIDTH: f32 = 76.0;
27
28/// <https://github.com/web-platform-tests/wpt/blob/9320b1f724632c52929a3fdb11bdaf65eafc7611/webdriver/tests/classic/set_window_rect/set.py#L287-L290>
29/// "A window size of 10x10px shouldn't be supported by any browser."
30#[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
31pub(crate) const MIN_WINDOW_INNER_SIZE: DeviceIntSize = DeviceIntSize::new(100, 100);
32
33static SERVOSHELL_WINDOW_ID: AtomicU64 = AtomicU64::new(0);
34
35#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
36pub(crate) struct ServoShellWindowId(u64);
37
38impl From<u64> for ServoShellWindowId {
39    fn from(value: u64) -> Self {
40        Self(value)
41    }
42}
43
44impl ServoShellWindowId {
45    #[cfg_attr(not(any(target_os = "android", target_env = "ohos")), expect(unused))]
46    pub(crate) fn next() -> ServoShellWindowId {
47        ServoShellWindowId(SERVOSHELL_WINDOW_ID.fetch_add(1, std::sync::atomic::Ordering::SeqCst))
48    }
49}
50
51pub(crate) struct ServoShellWindow {
52    /// The [`WebView`]s that have been added to this window.
53    pub(crate) webview_collection: RefCell<WebViewCollection>,
54    /// A handle to the [`PlatformWindow`] that servoshell is rendering in.
55    platform_window: Rc<dyn PlatformWindow>,
56    /// Whether or not this window should be closed at the end of the spin of the next event loop.
57    close_scheduled: Cell<bool>,
58    /// Whether or not the application interface needs to be updated.
59    needs_update: Cell<bool>,
60    /// Whether or not Servo needs to repaint its display. Currently this is global
61    /// because every `WebView` shares a `RenderingContext`.
62    needs_repaint: Cell<bool>,
63    /// List of webviews that have favicon textures which are not yet uploaded
64    /// to the GPU by egui.
65    pending_favicon_loads: RefCell<Vec<WebViewId>>,
66    /// Pending [`UserInterfaceCommand`] that have yet to be processed by the main loop.
67    pending_commands: RefCell<Vec<UserInterfaceCommand>>,
68}
69
70impl ServoShellWindow {
71    pub(crate) fn new(platform_window: Rc<dyn PlatformWindow>) -> Self {
72        Self {
73            webview_collection: Default::default(),
74            platform_window,
75            close_scheduled: Default::default(),
76            needs_update: Default::default(),
77            needs_repaint: Default::default(),
78            pending_favicon_loads: Default::default(),
79            pending_commands: Default::default(),
80        }
81    }
82
83    pub(crate) fn id(&self) -> ServoShellWindowId {
84        self.platform_window().id()
85    }
86
87    /// Must be called *after* `self` is in `state.windows`, otherwise it will panic.
88    pub(crate) fn create_and_activate_toplevel_webview(
89        &self,
90        state: Rc<RunningAppState>,
91        url: Url,
92    ) -> WebView {
93        let webview = self.create_toplevel_webview(state, url);
94        self.activate_webview(webview.id());
95        webview
96    }
97
98    /// Must be called *after* `self` is in `state.windows`, otherwise it will panic.
99    #[servo::servo_tracing::instrument(skip(self, state))]
100    pub(crate) fn create_toplevel_webview(&self, state: Rc<RunningAppState>, url: Url) -> WebView {
101        #[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(unused_mut))]
102        let mut webview_builder =
103            WebViewBuilder::new(state.servo(), self.platform_window.rendering_context())
104                .url(url)
105                .hidpi_scale_factor(self.platform_window.hidpi_scale_factor())
106                .user_content_manager(state.user_content_manager.clone())
107                .delegate(state.clone());
108
109        #[cfg(all(
110            feature = "gamepad",
111            not(any(target_os = "android", target_env = "ohos"))
112        ))]
113        if let Some(gamepad_delegate) = state.gamepad_delegate() {
114            webview_builder = webview_builder.gamepad_delegate(gamepad_delegate);
115        }
116
117        let webview = webview_builder.build();
118        webview.notify_theme_change(self.platform_window.theme());
119        self.add_webview(webview.clone());
120        // If `self` is not in `state.windows`, our notify_accessibility_tree_update() will panic.
121        if state.accessibility_active() {
122            // Activate accessibility in the WebView.
123            // There are two sites like this; this is the WebView creation site.
124            webview.set_accessibility_active(true);
125        }
126        webview
127    }
128
129    /// Repaint the focused [`WebView`].
130    pub(crate) fn repaint_webviews(&self) {
131        let Some(webview) = self.active_webview() else {
132            return;
133        };
134
135        self.platform_window()
136            .rendering_context()
137            .make_current()
138            .expect("Could not make PlatformWindow RenderingContext current");
139        webview.paint();
140        self.platform_window().rendering_context().present();
141    }
142
143    /// Whether or not this [`ServoShellWindow`] has any [`WebView`]s.
144    pub(crate) fn should_close(&self) -> bool {
145        self.webview_collection.borrow().is_empty() || self.close_scheduled.get()
146    }
147
148    pub(crate) fn contains_webview(&self, id: WebViewId) -> bool {
149        self.webview_collection.borrow().contains(id)
150    }
151
152    pub(crate) fn webview_by_id(&self, id: WebViewId) -> Option<WebView> {
153        self.webview_collection.borrow().get(id).cloned()
154    }
155
156    pub(crate) fn set_needs_update(&self) {
157        self.needs_update.set(true);
158    }
159
160    pub(crate) fn set_needs_repaint(&self) {
161        self.needs_repaint.set(true)
162    }
163
164    #[cfg_attr(target_os = "android", expect(dead_code))]
165    pub(crate) fn schedule_close(&self) {
166        self.close_scheduled.set(true)
167    }
168
169    pub(crate) fn platform_window(&self) -> Rc<dyn PlatformWindow> {
170        self.platform_window.clone()
171    }
172
173    pub(crate) fn focus(&self) {
174        self.platform_window.focus()
175    }
176
177    pub(crate) fn add_webview(&self, webview: WebView) {
178        self.webview_collection.borrow_mut().add(webview);
179        self.set_needs_update();
180        self.set_needs_repaint();
181    }
182
183    pub(crate) fn webview_ids(&self) -> Vec<WebViewId> {
184        self.webview_collection.borrow().creation_order.clone()
185    }
186
187    /// Returns all [`WebView`]s in creation order.
188    pub(crate) fn webviews(&self) -> Vec<(WebViewId, WebView)> {
189        self.webview_collection
190            .borrow()
191            .all_in_creation_order()
192            .map(|(id, webview)| (id, webview.clone()))
193            .collect()
194    }
195
196    pub(crate) fn activate_webview(&self, webview_id: WebViewId) {
197        self.webview_collection
198            .borrow_mut()
199            .activate_webview(webview_id);
200        self.set_needs_update();
201    }
202
203    #[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
204    pub(crate) fn activate_webview_by_index(&self, index_to_activate: usize) {
205        self.webview_collection
206            .borrow_mut()
207            .activate_webview_by_index(index_to_activate);
208        self.set_needs_update();
209    }
210
211    #[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
212    pub(crate) fn get_active_webview_index(&self) -> Option<usize> {
213        let active_id = self.webview_collection.borrow().active_id()?;
214        self.webviews()
215            .iter()
216            .position(|webview| webview.0 == active_id)
217    }
218
219    pub(crate) fn update_and_request_repaint_if_necessary(&self, state: &RunningAppState) {
220        let updated_user_interface = self.needs_update.take() &&
221            self.platform_window
222                .update_user_interface_state(state, self);
223
224        // Delegate handlers may have asked us to present or update painted WebView contents.
225        // Currently, egui-file-dialog dialogs need to be constantly redrawn or animations aren't fluid.
226        let needs_repaint = self.needs_repaint.take();
227        if updated_user_interface || needs_repaint {
228            self.platform_window.request_repaint(self);
229        }
230    }
231
232    /// Close the given [`WebView`] via its [`WebViewId`].
233    ///
234    /// Note: This can happen because we can trigger a close with a UI action and then get
235    /// the close notification via the [`WebViewDelegate`] later.
236    pub(crate) fn close_webview(&self, webview_id: WebViewId) {
237        let mut webview_collection = self.webview_collection.borrow_mut();
238        if webview_collection.remove(webview_id).is_none() {
239            return;
240        }
241        self.platform_window
242            .dismiss_embedder_controls_for_webview(webview_id);
243
244        self.set_needs_update();
245        self.set_needs_repaint();
246    }
247
248    pub(crate) fn notify_favicon_changed(&self, webview: WebView) {
249        self.pending_favicon_loads.borrow_mut().push(webview.id());
250        self.set_needs_repaint();
251    }
252
253    #[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
254    pub(crate) fn hidpi_scale_factor_changed(&self) {
255        let new_scale_factor = self.platform_window.hidpi_scale_factor();
256        for webview in self.webview_collection.borrow().values() {
257            webview.set_hidpi_scale_factor(new_scale_factor);
258        }
259    }
260
261    pub(crate) fn active_webview(&self) -> Option<WebView> {
262        self.webview_collection.borrow().active().cloned()
263    }
264
265    #[cfg_attr(
266        not(any(target_os = "android", target_env = "ohos")),
267        expect(dead_code)
268    )]
269    pub(crate) fn active_or_newest_webview(&self) -> Option<WebView> {
270        let webview_collection = self.webview_collection.borrow();
271        webview_collection
272            .active()
273            .or(webview_collection.newest())
274            .cloned()
275    }
276
277    /// Return a list of all webviews that have favicons that have not yet been loaded by egui.
278    #[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
279    pub(crate) fn take_pending_favicon_loads(&self) -> Vec<WebViewId> {
280        std::mem::take(&mut *self.pending_favicon_loads.borrow_mut())
281    }
282
283    pub(crate) fn show_embedder_control(
284        &self,
285        webview: WebView,
286        embedder_control: EmbedderControl,
287    ) {
288        self.platform_window
289            .show_embedder_control(webview.id(), embedder_control);
290        self.set_needs_update();
291        self.set_needs_repaint();
292    }
293
294    pub(crate) fn hide_embedder_control(
295        &self,
296        webview: WebView,
297        embedder_control: EmbedderControlId,
298    ) {
299        self.platform_window
300            .hide_embedder_control(webview.id(), embedder_control);
301        self.set_needs_update();
302        self.set_needs_repaint();
303    }
304
305    pub(crate) fn queue_user_interface_command(&self, command: UserInterfaceCommand) {
306        self.pending_commands.borrow_mut().push(command)
307    }
308
309    /// Takes any events generated during UI updates and performs their actions.
310    pub(crate) fn handle_interface_commands(
311        &self,
312        state: &Rc<RunningAppState>,
313        create_platform_window: Option<&dyn Fn(Url) -> Rc<dyn PlatformWindow>>,
314    ) {
315        let commands = std::mem::take(&mut *self.pending_commands.borrow_mut());
316        for event in commands {
317            match event {
318                UserInterfaceCommand::Go(location) => {
319                    self.set_needs_update();
320                    let Some(url) = location_bar_input_to_url(
321                        &location.clone(),
322                        &state.servoshell_preferences.searchpage,
323                    ) else {
324                        warn!("failed to parse location");
325                        break;
326                    };
327                    if let Some(active_webview) = self.active_webview() {
328                        active_webview.load(url.into_url());
329                    }
330                },
331                UserInterfaceCommand::Back => {
332                    if let Some(active_webview) = self.active_webview() {
333                        active_webview.go_back(1);
334                    }
335                },
336                UserInterfaceCommand::Forward => {
337                    if let Some(active_webview) = self.active_webview() {
338                        active_webview.go_forward(1);
339                    }
340                },
341                UserInterfaceCommand::Reload => {
342                    self.set_needs_update();
343                    if let Some(active_webview) = self.active_webview() {
344                        active_webview.reload();
345                    }
346                },
347                UserInterfaceCommand::ReloadAll => {
348                    for window in state.windows().values() {
349                        window.set_needs_update();
350                        for (_, webview) in window.webviews() {
351                            webview.reload();
352                        }
353                    }
354                },
355                UserInterfaceCommand::NewWebView => {
356                    self.set_needs_update();
357                    let url = Url::parse("servo:newtab").expect("Should always be able to parse");
358                    self.create_and_activate_toplevel_webview(state.clone(), url);
359                },
360                UserInterfaceCommand::CloseWebView(id) => {
361                    self.set_needs_update();
362                    self.close_webview(id);
363                },
364                UserInterfaceCommand::NewWindow => {
365                    if let Some(create_platform_window) = create_platform_window {
366                        let url = Url::parse("servo:newtab").unwrap();
367                        let platform_window = create_platform_window(url.clone());
368                        state.open_window(platform_window, url);
369                    }
370                },
371            }
372        }
373    }
374}
375
376/// A `PlatformWindow` abstracts away the differents kinds of platform windows that might
377/// be used in a servoshell execution. This currently includes headed (winit) and headless
378/// windows.
379pub(crate) trait PlatformWindow {
380    fn id(&self) -> ServoShellWindowId;
381    fn screen_geometry(&self) -> ScreenGeometry;
382    #[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
383    fn device_hidpi_scale_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel>;
384    fn hidpi_scale_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel>;
385    #[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
386    fn get_fullscreen(&self) -> bool;
387    /// Request that the `Window` rebuild its user interface, if it has one. This should
388    /// not repaint, but should prepare the user interface for painting when it is
389    /// actually requested.
390    #[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
391    fn rebuild_user_interface(&self, _: &RunningAppState, _: &ServoShellWindow) {}
392    /// Inform the `Window` that the state of a `WebView` has changed and that it should
393    /// do an incremental update of user interface state. Returns `true` if the user
394    /// interface actually changed and a rebuild  and repaint is needed, `false` otherwise.
395    fn update_user_interface_state(&self, _: &RunningAppState, _: &ServoShellWindow) -> bool {
396        false
397    }
398    /// Request that the window redraw itself. It is up to the window to do this
399    /// once the windowing system is ready. If this is a headless window, the redraw
400    /// will happen immediately.
401    fn request_repaint(&self, _: &ServoShellWindow);
402    /// Request a new outer size for the window, including external decorations.
403    /// This should be the same as `window.outerWidth` and `window.outerHeight``
404    fn request_resize(&self, webview: &WebView, outer_size: DeviceIntSize)
405    -> Option<DeviceIntSize>;
406    fn set_position(&self, _point: DeviceIntPoint) {}
407    fn set_fullscreen(&self, _state: bool) {}
408    fn set_cursor(&self, _cursor: Cursor) {}
409    #[cfg(all(
410        feature = "webxr",
411        not(any(target_os = "android", target_env = "ohos"))
412    ))]
413    fn new_glwindow(
414        &self,
415        event_loop: &winit::event_loop::ActiveEventLoop,
416    ) -> Rc<dyn servo::webxr::GlWindow>;
417    /// This returns [`RenderingContext`] matching the viewport.
418    fn rendering_context(&self) -> Rc<dyn RenderingContext>;
419    fn theme(&self) -> servo::Theme {
420        servo::Theme::Light
421    }
422    fn window_rect(&self) -> DeviceIndependentIntRect;
423    fn maximize(&self, _: &WebView) {}
424    fn focus(&self) {}
425    fn has_platform_focus(&self) -> bool {
426        true
427    }
428
429    fn show_embedder_control(&self, _: WebViewId, _: EmbedderControl) {}
430    fn hide_embedder_control(&self, _: WebViewId, _: EmbedderControlId) {}
431    fn dismiss_embedder_controls_for_webview(&self, _: WebViewId) {}
432    fn show_bluetooth_device_dialog(
433        &self,
434        _: WebViewId,
435        _request: BluetoothDeviceSelectionRequest,
436    ) {
437    }
438    fn show_permission_dialog(&self, _: WebViewId, _: PermissionRequest) {}
439    fn show_http_authentication_dialog(&self, _: WebViewId, _: AuthenticationRequest) {}
440
441    fn notify_input_event_handled(
442        &self,
443        _webview: &WebView,
444        _id: InputEventId,
445        _result: InputEventResult,
446    ) {
447    }
448
449    fn notify_media_session_event(&self, _: MediaSessionEvent) {}
450    fn notify_crashed(&self, _: WebView, _reason: String, _backtrace: Option<String>) {}
451    fn show_console_message(&self, _level: ConsoleLogLevel, _message: &str) {}
452
453    #[cfg(not(any(target_os = "android", target_env = "ohos")))]
454    /// If this window is a headed window, access the concrete type.
455    fn as_headed_window(&self) -> Option<&crate::desktop::headed_window::HeadedWindow> {
456        None
457    }
458
459    #[cfg(any(target_os = "android", target_env = "ohos"))]
460    /// If this window is a headed window, access the concrete type.
461    fn as_headed_window(&self) -> Option<&crate::egl::app::EmbeddedPlatformWindow> {
462        None
463    }
464
465    fn notify_accessibility_tree_update(&self, _: WebView, _: accesskit::TreeUpdate) {}
466}