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;
7
8use euclid::Scale;
9use servo::{
10    AuthenticationRequest, Cursor, DeviceIndependentIntRect, DeviceIndependentPixel,
11    DeviceIntPoint, DeviceIntSize, DevicePixel, EmbedderControl, EmbedderControlId, GenericSender,
12    InputEventId, InputEventResult, MediaSessionEvent, PermissionRequest, RenderingContext,
13    ScreenGeometry, WebView, WebViewBuilder, WebViewId,
14};
15use url::Url;
16
17use crate::running_app_state::{RunningAppState, UserInterfaceCommand, WebViewCollection};
18
19// This should vary by zoom level and maybe actual text size (focused or under cursor)
20#[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
21pub(crate) const LINE_HEIGHT: f32 = 76.0;
22#[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
23pub(crate) const LINE_WIDTH: f32 = 76.0;
24
25/// <https://github.com/web-platform-tests/wpt/blob/9320b1f724632c52929a3fdb11bdaf65eafc7611/webdriver/tests/classic/set_window_rect/set.py#L287-L290>
26/// "A window size of 10x10px shouldn't be supported by any browser."
27#[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
28pub(crate) const MIN_WINDOW_INNER_SIZE: DeviceIntSize = DeviceIntSize::new(100, 100);
29
30#[derive(Copy, Clone, Eq, Hash, PartialEq)]
31pub(crate) struct ServoShellWindowId(u64);
32
33impl From<u64> for ServoShellWindowId {
34    fn from(value: u64) -> Self {
35        Self(value)
36    }
37}
38
39pub(crate) struct ServoShellWindow {
40    /// The [`WebView`]s that have been added to this window.
41    pub(crate) webview_collection: RefCell<WebViewCollection>,
42    /// A handle to the [`PlatformWindow`] that servoshell is rendering in.
43    platform_window: Rc<dyn PlatformWindow>,
44    /// Whether or not this window should be closed at the end of the spin of the next event loop.
45    close_scheduled: Cell<bool>,
46    /// Whether or not the application interface needs to be updated.
47    needs_update: Cell<bool>,
48    /// Whether or not Servo needs to repaint its display. Currently this is global
49    /// because every `WebView` shares a `RenderingContext`.
50    needs_repaint: Cell<bool>,
51    /// List of webviews that have favicon textures which are not yet uploaded
52    /// to the GPU by egui.
53    pending_favicon_loads: RefCell<Vec<WebViewId>>,
54}
55
56impl ServoShellWindow {
57    pub(crate) fn new(platform_window: Rc<dyn PlatformWindow>) -> Self {
58        Self {
59            webview_collection: Default::default(),
60            platform_window,
61            close_scheduled: Default::default(),
62            needs_update: Default::default(),
63            needs_repaint: Default::default(),
64            pending_favicon_loads: Default::default(),
65        }
66    }
67
68    pub(crate) fn id(&self) -> ServoShellWindowId {
69        self.platform_window().id()
70    }
71
72    pub(crate) fn create_and_activate_toplevel_webview(
73        &self,
74        state: Rc<RunningAppState>,
75        url: Url,
76    ) -> WebView {
77        let webview = self.create_toplevel_webview(state, url);
78        self.activate_webview(webview.id());
79        webview
80    }
81
82    pub(crate) fn create_toplevel_webview(&self, state: Rc<RunningAppState>, url: Url) -> WebView {
83        let webview = WebViewBuilder::new(state.servo(), self.platform_window.rendering_context())
84            .url(url)
85            .hidpi_scale_factor(self.platform_window.hidpi_scale_factor())
86            .delegate(state.clone())
87            .build();
88
89        webview.notify_theme_change(self.platform_window.theme());
90        self.add_webview(webview.clone());
91        webview
92    }
93
94    /// Repaint the focused [`WebView`].
95    pub(crate) fn repaint_webviews(&self) {
96        let Some(webview) = self.active_webview() else {
97            return;
98        };
99
100        self.platform_window()
101            .rendering_context()
102            .make_current()
103            .expect("Could not make PlatformWindow RenderingContext current");
104        webview.paint();
105        self.platform_window().rendering_context().present();
106    }
107
108    /// Whether or not this [`ServoShellWindow`] has any [`WebView`]s.
109    pub(crate) fn should_close(&self) -> bool {
110        self.webview_collection.borrow().is_empty() || self.close_scheduled.get()
111    }
112
113    pub(crate) fn contains_webview(&self, id: WebViewId) -> bool {
114        self.webview_collection.borrow().contains(id)
115    }
116
117    pub(crate) fn webview_by_id(&self, id: WebViewId) -> Option<WebView> {
118        self.webview_collection.borrow().get(id).cloned()
119    }
120
121    pub(crate) fn set_needs_update(&self) {
122        self.needs_update.set(true);
123    }
124
125    pub(crate) fn set_needs_repaint(&self) {
126        self.needs_repaint.set(true)
127    }
128
129    #[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
130    pub(crate) fn schedule_close(&self) {
131        self.close_scheduled.set(true)
132    }
133
134    pub(crate) fn platform_window(&self) -> Rc<dyn PlatformWindow> {
135        self.platform_window.clone()
136    }
137
138    pub(crate) fn focused(&self) -> bool {
139        self.platform_window.focused()
140    }
141
142    pub(crate) fn add_webview(&self, webview: WebView) {
143        self.webview_collection.borrow_mut().add(webview);
144        self.set_needs_update();
145        self.set_needs_repaint();
146    }
147
148    pub(crate) fn webview_ids(&self) -> Vec<WebViewId> {
149        self.webview_collection.borrow().creation_order.clone()
150    }
151
152    /// Returns all [`WebView`]s in creation order.
153    pub(crate) fn webviews(&self) -> Vec<(WebViewId, WebView)> {
154        self.webview_collection
155            .borrow()
156            .all_in_creation_order()
157            .map(|(id, webview)| (id, webview.clone()))
158            .collect()
159    }
160
161    pub(crate) fn activate_webview(&self, webview_id: WebViewId) {
162        self.webview_collection
163            .borrow_mut()
164            .activate_webview(webview_id);
165        self.set_needs_update();
166    }
167
168    #[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
169    pub(crate) fn activate_webview_by_index(&self, index_to_activate: usize) {
170        self.webview_collection
171            .borrow_mut()
172            .activate_webview_by_index(index_to_activate);
173        self.set_needs_update();
174    }
175
176    #[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
177    pub(crate) fn get_active_webview_index(&self) -> Option<usize> {
178        let active_id = self.webview_collection.borrow().active_id()?;
179        self.webviews()
180            .iter()
181            .position(|webview| webview.0 == active_id)
182    }
183
184    pub(crate) fn update_and_request_repaint_if_necessary(&self, state: &RunningAppState) {
185        let updated_user_interface = self.needs_update.take() &&
186            self.platform_window
187                .update_user_interface_state(state, self);
188
189        // Delegate handlers may have asked us to present or update compositor contents.
190        // Currently, egui-file-dialog dialogs need to be constantly redrawn or animations aren't fluid.
191        let needs_repaint = self.needs_repaint.take();
192        if updated_user_interface || needs_repaint {
193            self.platform_window.request_repaint(self);
194        }
195    }
196
197    /// Close the given [`WebView`] via its [`WebViewId`].
198    ///
199    /// Note: This can happen because we can trigger a close with a UI action and then get
200    /// the close notification via the [`WebViewDelegate`] later.
201    pub(crate) fn close_webview(&self, webview_id: WebViewId) {
202        let mut webview_collection = self.webview_collection.borrow_mut();
203        if webview_collection.remove(webview_id).is_none() {
204            return;
205        }
206        self.platform_window
207            .dismiss_embedder_controls_for_webview(webview_id);
208
209        self.set_needs_update();
210        self.set_needs_repaint();
211    }
212
213    pub(crate) fn notify_favicon_changed(&self, webview: WebView) {
214        self.pending_favicon_loads.borrow_mut().push(webview.id());
215        self.set_needs_repaint();
216    }
217
218    #[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
219    pub(crate) fn hidpi_scale_factor_changed(&self) {
220        let new_scale_factor = self.platform_window.hidpi_scale_factor();
221        for webview in self.webview_collection.borrow().values() {
222            webview.set_hidpi_scale_factor(new_scale_factor);
223        }
224    }
225
226    pub(crate) fn active_webview(&self) -> Option<WebView> {
227        self.webview_collection.borrow().active().cloned()
228    }
229
230    #[cfg_attr(
231        not(any(target_os = "android", target_env = "ohos")),
232        expect(dead_code)
233    )]
234    pub(crate) fn active_or_newest_webview(&self) -> Option<WebView> {
235        let webview_collection = self.webview_collection.borrow();
236        webview_collection
237            .active()
238            .or(webview_collection.newest())
239            .cloned()
240    }
241
242    /// Return a list of all webviews that have favicons that have not yet been loaded by egui.
243    #[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
244    pub(crate) fn take_pending_favicon_loads(&self) -> Vec<WebViewId> {
245        std::mem::take(&mut *self.pending_favicon_loads.borrow_mut())
246    }
247
248    pub(crate) fn show_embedder_control(
249        &self,
250        webview: WebView,
251        embedder_control: EmbedderControl,
252    ) {
253        self.platform_window
254            .show_embedder_control(webview.id(), embedder_control);
255        self.set_needs_update();
256        self.set_needs_repaint();
257    }
258
259    pub(crate) fn hide_embedder_control(
260        &self,
261        webview: WebView,
262        embedder_control: EmbedderControlId,
263    ) {
264        self.platform_window
265            .hide_embedder_control(webview.id(), embedder_control);
266        self.set_needs_update();
267        self.set_needs_repaint();
268    }
269}
270
271/// A `PlatformWindow` abstracts away the differents kinds of platform windows that might
272/// be used in a servoshell execution. This currently includes headed (winit) and headless
273/// windows.
274pub(crate) trait PlatformWindow {
275    fn id(&self) -> ServoShellWindowId;
276    fn screen_geometry(&self) -> ScreenGeometry;
277    #[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
278    fn device_hidpi_scale_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel>;
279    fn hidpi_scale_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel>;
280    #[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
281    fn get_fullscreen(&self) -> bool;
282    /// Request that the `Window` rebuild its user interface, if it has one. This should
283    /// not repaint, but should prepare the user interface for painting when it is
284    /// actually requested.
285    #[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
286    fn rebuild_user_interface(&self, _: &RunningAppState, _: &ServoShellWindow) {}
287    /// Inform the `Window` that the state of a `WebView` has changed and that it should
288    /// do an incremental update of user interface state. Returns `true` if the user
289    /// interface actually changed and a rebuild  and repaint is needed, `false` otherwise.
290    fn update_user_interface_state(&self, _: &RunningAppState, _: &ServoShellWindow) -> bool {
291        false
292    }
293    /// Handle a winit [`WindowEvent`]. Returns `true` if the event loop should continue
294    /// and `false` otherwise.
295    ///
296    /// TODO: This should be handled internally in the winit window if possible so that it
297    /// makes more sense when we are mixing headed and headless windows.
298    #[cfg(not(any(target_os = "android", target_env = "ohos")))]
299    fn handle_winit_window_event(
300        &self,
301        _: Rc<RunningAppState>,
302        _: &ServoShellWindow,
303        _: winit::event::WindowEvent,
304    ) {
305    }
306    /// Handle a winit [`AppEvent`]. Returns `true` if the event loop should continue and
307    /// `false` otherwise.
308    ///
309    /// TODO: This should be handled internally in the winit window if possible so that it
310    /// makes more sense when we are mixing headed and headless windows.
311    #[cfg(not(any(target_os = "android", target_env = "ohos")))]
312    fn handle_winit_app_event(&self, _: crate::desktop::event_loop::AppEvent) {}
313    fn take_user_interface_commands(&self) -> Vec<UserInterfaceCommand> {
314        Default::default()
315    }
316    /// Request that the window redraw itself. It is up to the window to do this
317    /// once the windowing system is ready. If this is a headless window, the redraw
318    /// will happen immediately.
319    fn request_repaint(&self, _: &ServoShellWindow);
320    /// Request a new outer size for the window, including external decorations.
321    /// This should be the same as `window.outerWidth` and `window.outerHeight``
322    fn request_resize(&self, webview: &WebView, outer_size: DeviceIntSize)
323    -> Option<DeviceIntSize>;
324    fn set_position(&self, _point: DeviceIntPoint) {}
325    fn set_fullscreen(&self, _state: bool) {}
326    fn set_cursor(&self, _cursor: Cursor) {}
327    #[cfg(all(
328        feature = "webxr",
329        not(any(target_os = "android", target_env = "ohos"))
330    ))]
331    fn new_glwindow(
332        &self,
333        event_loop: &winit::event_loop::ActiveEventLoop,
334    ) -> Rc<dyn servo::webxr::GlWindow>;
335    /// This returns [`RenderingContext`] matching the viewport.
336    fn rendering_context(&self) -> Rc<dyn RenderingContext>;
337    fn theme(&self) -> servo::Theme {
338        servo::Theme::Light
339    }
340    fn window_rect(&self) -> DeviceIndependentIntRect;
341    fn maximize(&self, _: &WebView) {}
342    fn focused(&self) -> bool;
343
344    fn show_embedder_control(&self, _: WebViewId, _: EmbedderControl) {}
345    fn hide_embedder_control(&self, _: WebViewId, _: EmbedderControlId) {}
346    fn dismiss_embedder_controls_for_webview(&self, _: WebViewId) {}
347    fn show_bluetooth_device_dialog(
348        &self,
349        _: WebViewId,
350        _devices: Vec<String>,
351        _: GenericSender<Option<String>>,
352    ) {
353    }
354    fn show_permission_dialog(&self, _: WebViewId, _: PermissionRequest) {}
355    fn show_http_authentication_dialog(&self, _: WebViewId, _: AuthenticationRequest) {}
356
357    fn notify_input_event_handled(
358        &self,
359        _webview: &WebView,
360        _id: InputEventId,
361        _result: InputEventResult,
362    ) {
363    }
364
365    fn notify_media_session_event(&self, _: MediaSessionEvent) {}
366    fn notify_crashed(&self, _: WebView, _reason: String, _backtrace: Option<String>) {}
367}