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