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