Skip to main content

servoshell/desktop/
headed_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
5//! A winit window implementation.
6
7#![deny(clippy::panic)]
8#![deny(clippy::unwrap_used)]
9
10use std::cell::{Cell, RefCell};
11use std::collections::HashMap;
12use std::env;
13use std::rc::Rc;
14use std::time::Duration;
15
16use euclid::{Angle, Length, Point2D, Rect, Rotation3D, Scale, Size2D, UnknownUnit, Vector3D};
17use keyboard_types::ShortcutMatcher;
18use log::{debug, info};
19use raw_window_handle::{HasDisplayHandle, HasWindowHandle, RawWindowHandle};
20use servo::{
21    AuthenticationRequest, BluetoothDeviceSelectionRequest, Cursor, DeviceIndependentIntRect,
22    DeviceIndependentPixel, DeviceIntPoint, DeviceIntRect, DeviceIntSize, DevicePixel, DevicePoint,
23    EmbedderControl, EmbedderControlId, ImeEvent, InputEvent, InputEventId, InputEventResult,
24    InputMethodControl, Key, KeyState, KeyboardEvent, Modifiers, MouseButton as ServoMouseButton,
25    MouseButtonAction, MouseButtonEvent, MouseLeftViewportEvent, MouseMoveEvent, NamedKey,
26    OffscreenRenderingContext, PermissionRequest, RenderingContext, ScreenGeometry, Theme,
27    TouchEvent, TouchEventType, TouchId, TouchPointerType, WebRenderDebugOption, WebView,
28    WebViewId, WheelDelta, WheelEvent, WheelMode, WindowRenderingContext,
29    convert_rect_to_css_pixel,
30};
31use url::Url;
32use winit::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize};
33use winit::event::{
34    ElementState, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase, WindowEvent,
35};
36use winit::event_loop::{ActiveEventLoop, EventLoopProxy};
37use winit::keyboard::{Key as LogicalKey, ModifiersState, NamedKey as WinitNamedKey};
38#[cfg(target_os = "linux")]
39use winit::platform::wayland::WindowAttributesExtWayland;
40#[cfg(any(target_os = "linux", target_os = "windows"))]
41use winit::window::Icon;
42#[cfg(target_os = "macos")]
43use {
44    objc2_app_kit::{NSColorSpace, NSView},
45    objc2_foundation::MainThreadMarker,
46};
47
48use super::geometry::{winit_position_to_euclid_point, winit_size_to_euclid_size};
49use super::keyutils::{CMD_OR_ALT, keyboard_event_from_winit};
50use crate::desktop::accelerated_gl_media::setup_gl_accelerated_media;
51use crate::desktop::dialog::Dialog;
52use crate::desktop::event_loop::AppEvent;
53use crate::desktop::gui::Gui;
54use crate::desktop::keyutils::CMD_OR_CONTROL;
55use crate::prefs::ServoShellPreferences;
56use crate::running_app_state::{RunningAppState, UserInterfaceCommand};
57use crate::window::{
58    LINE_HEIGHT, LINE_WIDTH, MIN_WINDOW_INNER_SIZE, PlatformWindow, ServoShellWindow,
59    ServoShellWindowId,
60};
61
62pub(crate) const INITIAL_WINDOW_TITLE: &str = "Servo";
63
64pub struct HeadedWindow {
65    /// The egui interface that is responsible for showing the user interface elements of
66    /// this headed `Window`.
67    gui: RefCell<Gui>,
68    screen_size: Size2D<u32, DeviceIndependentPixel>,
69    webview_relative_mouse_point: Cell<Point2D<f32, DevicePixel>>,
70    /// The inner size of the window in physical pixels which excludes OS decorations.
71    /// It equals viewport size + (0, toolbar height).
72    inner_size: Cell<PhysicalSize<u32>>,
73    fullscreen: Cell<bool>,
74    device_pixel_ratio_override: Option<f32>,
75    xr_window_poses: RefCell<Vec<Rc<XRWindowPose>>>,
76    modifiers_state: Cell<ModifiersState>,
77    /// The `RenderingContext` of Servo itself. This is used to render Servo results
78    /// temporarily until they can be blitted into the egui scene.
79    rendering_context: Rc<OffscreenRenderingContext>,
80    /// The RenderingContext that renders directly onto the Window. This is used as
81    /// the target of egui rendering and also where Servo rendering results are finally
82    /// blitted.
83    window_rendering_context: Rc<WindowRenderingContext>,
84    /// A helper that simulates touch events when the `--simulate-touch-events` flag
85    /// is enabled.
86    touch_event_simulator: Option<TouchEventSimulator>,
87    /// Keyboard events that have been sent to Servo that have still not been handled yet.
88    /// When these are handled, they will optionally be used to trigger keybindings that
89    /// are overridable by web content.
90    pending_keyboard_events: RefCell<HashMap<InputEventId, KeyboardEvent>>,
91    // Keep this as the last field of the struct to ensure that the rendering context is
92    // dropped first.
93    // (https://github.com/servo/servo/issues/36711)
94    winit_window: winit::window::Window,
95    /// The last title set on this window. We need to store this value here, as `winit::Window::title`
96    /// is not supported very many platforms.
97    last_title: RefCell<String>,
98    /// The current set of open dialogs.
99    dialogs: RefCell<HashMap<WebViewId, Vec<Dialog>>>,
100    /// The [`EmbedderControlId`] of the currently showing [`InputMethod`] interfaces,
101    /// if one is showing.
102    visible_input_method: Cell<Option<EmbedderControlId>>,
103    /// The position of the mouse cursor after the most recent `MouseMove` event.
104    last_mouse_position: Cell<Option<Point2D<f32, DeviceIndependentPixel>>>,
105}
106
107impl HeadedWindow {
108    #[servo::servo_tracing::instrument(level = "debug", name = "HeadedWindow::new", skip_all)]
109    pub(crate) fn new(
110        servoshell_preferences: &ServoShellPreferences,
111        event_loop: &ActiveEventLoop,
112        event_loop_proxy: EventLoopProxy<AppEvent>,
113        initial_url: Url,
114    ) -> Rc<Self> {
115        let no_native_titlebar = servoshell_preferences.no_native_titlebar;
116        let inner_size = servoshell_preferences.initial_window_size;
117        let window_attr = winit::window::Window::default_attributes()
118            .with_title(INITIAL_WINDOW_TITLE.to_string())
119            .with_decorations(!no_native_titlebar)
120            .with_transparent(no_native_titlebar)
121            .with_inner_size(LogicalSize::new(inner_size.width, inner_size.height))
122            .with_min_inner_size(LogicalSize::new(
123                MIN_WINDOW_INNER_SIZE.width,
124                MIN_WINDOW_INNER_SIZE.height,
125            ))
126            // Must be invisible at startup; accesskit_winit setup needs to
127            // happen before the window is shown for the first time.
128            .with_visible(false);
129
130        // Set a name so it can be pinned to taskbars in Linux.
131        #[cfg(target_os = "linux")]
132        let window_attr = window_attr.with_name("org.servo.Servo", "Servo");
133
134        #[allow(deprecated)]
135        let winit_window = event_loop
136            .create_window(window_attr)
137            .expect("Failed to create window.");
138
139        #[cfg(any(target_os = "linux", target_os = "windows"))]
140        {
141            let icon_bytes = include_bytes!("../../../resources/servo_64.png");
142            winit_window.set_window_icon(Some(load_icon(icon_bytes)));
143        }
144
145        let window_handle = winit_window
146            .window_handle()
147            .expect("winit window did not have a window handle");
148        HeadedWindow::force_srgb_color_space(window_handle.as_raw());
149
150        let monitor = winit_window
151            .current_monitor()
152            .or_else(|| winit_window.available_monitors().nth(0))
153            .expect("No monitor detected");
154
155        let (screen_size, screen_scale) = servoshell_preferences.screen_size_override.map_or_else(
156            || (monitor.size(), winit_window.scale_factor()),
157            |size| (PhysicalSize::new(size.width, size.height), 1.0),
158        );
159        let screen_scale: Scale<f64, DeviceIndependentPixel, DevicePixel> =
160            Scale::new(screen_scale);
161        let screen_size = (winit_size_to_euclid_size(screen_size).to_f64() / screen_scale).to_u32();
162        let inner_size = winit_window.inner_size();
163
164        let display_handle = event_loop
165            .display_handle()
166            .expect("could not get display handle from window");
167        let window_handle = winit_window
168            .window_handle()
169            .expect("could not get window handle from window");
170        let window_rendering_context = Rc::new(
171            WindowRenderingContext::new(display_handle, window_handle, inner_size)
172                .expect("Could not create RenderingContext for Window"),
173        );
174
175        // Setup for GL accelerated media handling. This is only active on certain Linux platforms
176        // and Windows.
177        {
178            let details = window_rendering_context.surfman_details();
179            setup_gl_accelerated_media(details.0, details.1);
180        }
181
182        // Make sure the gl context is made current.
183        window_rendering_context
184            .make_current()
185            .expect("Could not make window RenderingContext current");
186
187        let rendering_context = Rc::new(window_rendering_context.offscreen_context(inner_size));
188        let gui = RefCell::new(Gui::new(
189            &winit_window,
190            event_loop,
191            event_loop_proxy,
192            rendering_context.clone(),
193            initial_url,
194        ));
195
196        debug!("Created window {:?}", winit_window.id());
197        Rc::new(HeadedWindow {
198            gui,
199            winit_window,
200            webview_relative_mouse_point: Cell::new(Point2D::zero()),
201            fullscreen: Cell::new(false),
202            inner_size: Cell::new(inner_size),
203            screen_size,
204            device_pixel_ratio_override: servoshell_preferences.device_pixel_ratio_override,
205            xr_window_poses: RefCell::new(vec![]),
206            modifiers_state: Cell::new(ModifiersState::empty()),
207            window_rendering_context,
208            touch_event_simulator: servoshell_preferences
209                .simulate_touch_events
210                .then(Default::default),
211            pending_keyboard_events: Default::default(),
212            rendering_context,
213            last_title: RefCell::new(String::from(INITIAL_WINDOW_TITLE)),
214            dialogs: Default::default(),
215            visible_input_method: Default::default(),
216            last_mouse_position: Default::default(),
217        })
218    }
219
220    pub(crate) fn winit_window(&self) -> &winit::window::Window {
221        &self.winit_window
222    }
223
224    fn handle_keyboard_input(
225        &self,
226        state: Rc<RunningAppState>,
227        window: &ServoShellWindow,
228        winit_event: KeyEvent,
229    ) {
230        // First, handle servoshell key bindings that are not overridable by, or visible to, the page.
231        let keyboard_event = keyboard_event_from_winit(&winit_event, self.modifiers_state.get());
232        if self.handle_intercepted_key_bindings(state, window, &keyboard_event) {
233            return;
234        }
235
236        // Then we deliver character and keyboard events to the page in the active webview.
237        let Some(webview) = window.active_webview() else {
238            return;
239        };
240
241        for xr_window_pose in self.xr_window_poses.borrow().iter() {
242            xr_window_pose.handle_xr_rotation(&winit_event, self.modifiers_state.get());
243            xr_window_pose.handle_xr_translation(&keyboard_event);
244        }
245
246        let id = webview.notify_input_event(InputEvent::Keyboard(keyboard_event.clone()));
247        self.pending_keyboard_events
248            .borrow_mut()
249            .insert(id, keyboard_event);
250    }
251
252    /// Helper function to handle a click
253    fn handle_mouse_button_event(
254        &self,
255        webview: &WebView,
256        button: MouseButton,
257        action: ElementState,
258    ) {
259        // `point` can be outside viewport, such as at toolbar with negative y-coordinate.
260        let point = self.webview_relative_mouse_point.get();
261        let webview_rect: Rect<_, _> = webview.size().into();
262        if !webview_rect.contains(point) {
263            return;
264        }
265
266        if self
267            .touch_event_simulator
268            .as_ref()
269            .is_some_and(|touch_event_simulator| {
270                touch_event_simulator
271                    .maybe_consume_move_button_event(webview, button, action, point)
272            })
273        {
274            return;
275        }
276
277        let mouse_button = match &button {
278            MouseButton::Left => ServoMouseButton::Left,
279            MouseButton::Right => ServoMouseButton::Right,
280            MouseButton::Middle => ServoMouseButton::Middle,
281            MouseButton::Back => ServoMouseButton::Back,
282            MouseButton::Forward => ServoMouseButton::Forward,
283            MouseButton::Other(value) => ServoMouseButton::Other(*value),
284        };
285
286        let action = match action {
287            ElementState::Pressed => MouseButtonAction::Down,
288            ElementState::Released => MouseButtonAction::Up,
289        };
290
291        webview.notify_input_event(InputEvent::MouseButton(MouseButtonEvent::new(
292            action,
293            mouse_button,
294            point.into(),
295        )));
296    }
297
298    /// Helper function to handle mouse move events.
299    fn handle_mouse_move_event(&self, webview: &WebView, position: PhysicalPosition<f64>) {
300        let mut point = winit_position_to_euclid_point(position).to_f32();
301        point.y -= (self.toolbar_height() * self.hidpi_scale_factor()).0;
302
303        let previous_point = self.webview_relative_mouse_point.get();
304        self.webview_relative_mouse_point.set(point);
305
306        let webview_rect: Rect<_, _> = webview.size().into();
307        if !webview_rect.contains(point) {
308            if webview_rect.contains(previous_point) {
309                webview.notify_input_event(InputEvent::MouseLeftViewport(
310                    MouseLeftViewportEvent::default(),
311                ));
312            }
313            return;
314        }
315
316        if self
317            .touch_event_simulator
318            .as_ref()
319            .is_some_and(|touch_event_simulator| {
320                touch_event_simulator.maybe_consume_mouse_move_event(webview, point)
321            })
322        {
323            return;
324        }
325
326        webview.notify_input_event(InputEvent::MouseMove(MouseMoveEvent::new(point.into())));
327    }
328
329    /// Handle key events before sending them to Servo.
330    fn handle_intercepted_key_bindings(
331        &self,
332        state: Rc<RunningAppState>,
333        window: &ServoShellWindow,
334        key_event: &KeyboardEvent,
335    ) -> bool {
336        let Some(active_webview) = window.active_webview() else {
337            return false;
338        };
339
340        let mut handled = true;
341        ShortcutMatcher::from_event(key_event.event.clone())
342            .shortcut(CMD_OR_CONTROL, 'W', || {
343                window.close_webview(active_webview.id());
344            })
345            .shortcut(CMD_OR_CONTROL, 'P', || {
346                let rate = env::var("SAMPLING_RATE")
347                    .ok()
348                    .and_then(|s| s.parse().ok())
349                    .unwrap_or(10);
350                let duration = env::var("SAMPLING_DURATION")
351                    .ok()
352                    .and_then(|s| s.parse().ok())
353                    .unwrap_or(10);
354                active_webview.toggle_sampling_profiler(
355                    Duration::from_millis(rate),
356                    Duration::from_secs(duration),
357                );
358            })
359            .shortcut(CMD_OR_CONTROL, 'X', || {
360                active_webview
361                    .notify_input_event(InputEvent::EditingAction(servo::EditingActionEvent::Cut));
362            })
363            .shortcut(CMD_OR_CONTROL, 'C', || {
364                active_webview
365                    .notify_input_event(InputEvent::EditingAction(servo::EditingActionEvent::Copy));
366            })
367            .shortcut(CMD_OR_CONTROL, 'V', || {
368                active_webview.notify_input_event(InputEvent::EditingAction(
369                    servo::EditingActionEvent::Paste,
370                ));
371            })
372            .shortcut(Modifiers::CONTROL, Key::Named(NamedKey::F9), || {
373                active_webview.capture_webrender();
374            })
375            .shortcut(Modifiers::CONTROL, Key::Named(NamedKey::F10), || {
376                active_webview.toggle_webrender_debugging(WebRenderDebugOption::RenderTargetDebug);
377            })
378            .shortcut(Modifiers::CONTROL, Key::Named(NamedKey::F11), || {
379                active_webview.toggle_webrender_debugging(WebRenderDebugOption::TextureCacheDebug);
380            })
381            .shortcut(Modifiers::CONTROL, Key::Named(NamedKey::F12), || {
382                active_webview.toggle_webrender_debugging(WebRenderDebugOption::Profiler);
383            })
384            .shortcut(CMD_OR_ALT, Key::Named(NamedKey::ArrowRight), || {
385                active_webview.go_forward(1);
386            })
387            .optional_shortcut(
388                cfg!(not(target_os = "windows")),
389                CMD_OR_CONTROL,
390                ']',
391                || {
392                    active_webview.go_forward(1);
393                },
394            )
395            .shortcut(CMD_OR_ALT, Key::Named(NamedKey::ArrowLeft), || {
396                active_webview.go_back(1);
397            })
398            .optional_shortcut(
399                cfg!(not(target_os = "windows")),
400                CMD_OR_CONTROL,
401                '[',
402                || {
403                    active_webview.go_back(1);
404                },
405            )
406            .optional_shortcut(
407                self.get_fullscreen(),
408                Modifiers::empty(),
409                Key::Named(NamedKey::Escape),
410                || active_webview.exit_fullscreen(),
411            )
412            // Select the first 8 tabs via shortcuts
413            .shortcut(CMD_OR_CONTROL, '1', || window.activate_webview_by_index(0))
414            .shortcut(CMD_OR_CONTROL, '2', || window.activate_webview_by_index(1))
415            .shortcut(CMD_OR_CONTROL, '3', || window.activate_webview_by_index(2))
416            .shortcut(CMD_OR_CONTROL, '4', || window.activate_webview_by_index(3))
417            .shortcut(CMD_OR_CONTROL, '5', || window.activate_webview_by_index(4))
418            .shortcut(CMD_OR_CONTROL, '6', || window.activate_webview_by_index(5))
419            .shortcut(CMD_OR_CONTROL, '7', || window.activate_webview_by_index(6))
420            .shortcut(CMD_OR_CONTROL, '8', || window.activate_webview_by_index(7))
421            // Cmd/Ctrl 9 is a bit different in that it focuses the last tab instead of the 9th
422            .shortcut(CMD_OR_CONTROL, '9', || {
423                let len = window.webviews().len();
424                if len > 0 {
425                    window.activate_webview_by_index(len - 1)
426                }
427            })
428            .shortcut(Modifiers::CONTROL, Key::Named(NamedKey::PageDown), || {
429                if let Some(index) = window.get_active_webview_index() {
430                    window.activate_webview_by_index((index + 1) % window.webviews().len())
431                }
432            })
433            .shortcut(Modifiers::CONTROL, Key::Named(NamedKey::PageUp), || {
434                if let Some(index) = window.get_active_webview_index() {
435                    let len = window.webviews().len();
436                    window.activate_webview_by_index((index + len - 1) % len);
437                }
438            })
439            .shortcut(CMD_OR_CONTROL, 'T', || {
440                window.create_and_activate_toplevel_webview(
441                    state.clone(),
442                    Url::parse("servo:newtab")
443                        .expect("Should be able to unconditionally parse 'servo:newtab' as URL"),
444                );
445            })
446            .shortcut(CMD_OR_CONTROL, 'Q', || state.schedule_exit())
447            .otherwise(|| handled = false);
448        handled
449    }
450
451    #[cfg_attr(not(target_os = "macos"), expect(unused_variables))]
452    fn force_srgb_color_space(window_handle: RawWindowHandle) {
453        #[cfg(target_os = "macos")]
454        {
455            if let RawWindowHandle::AppKit(handle) = window_handle {
456                assert!(MainThreadMarker::new().is_some());
457                unsafe {
458                    let view = handle.ns_view.cast::<NSView>().as_ref();
459                    view.window()
460                        .expect("Should have a window")
461                        .setColorSpace(Some(&NSColorSpace::sRGBColorSpace()));
462                }
463            }
464        }
465    }
466
467    fn show_ime(&self, control_id: EmbedderControlId, input_method: InputMethodControl) {
468        self.visible_input_method.set(Some(control_id));
469
470        let position = input_method.position();
471        self.winit_window.set_ime_allowed(true);
472        self.winit_window.set_ime_cursor_area(
473            LogicalPosition::new(
474                position.min.x,
475                position.min.y + (self.toolbar_height().0 as i32),
476            ),
477            LogicalSize::new(
478                position.max.x - position.min.x,
479                position.max.y - position.min.y,
480            ),
481        );
482    }
483
484    pub(crate) fn for_each_active_dialog(
485        &self,
486        window: &ServoShellWindow,
487        callback: impl Fn(&mut Dialog) -> bool,
488    ) {
489        let Some(active_webview) = window.active_webview() else {
490            return;
491        };
492        let mut dialogs = self.dialogs.borrow_mut();
493        let Some(dialogs) = dialogs.get_mut(&active_webview.id()) else {
494            return;
495        };
496        if dialogs.is_empty() {
497            return;
498        }
499
500        // If a dialog is open, clear any Servo cursor. TODO: This should restore the
501        // cursor too, when all dialogs close. In general, we need a better cursor
502        // management strategy.
503        self.set_cursor(Cursor::Default);
504        dialogs.retain_mut(callback);
505    }
506
507    fn add_dialog(&self, webview_id: WebViewId, dialog: Dialog) {
508        self.dialogs
509            .borrow_mut()
510            .entry(webview_id)
511            .or_default()
512            .push(dialog)
513    }
514
515    fn remove_dialog(&self, webview_id: WebViewId, embedder_control_id: EmbedderControlId) {
516        let mut dialogs = self.dialogs.borrow_mut();
517        if let Some(dialogs) = dialogs.get_mut(&webview_id) {
518            dialogs.retain(|dialog| dialog.embedder_control_id() != Some(embedder_control_id));
519        }
520        dialogs.retain(|_, dialogs| !dialogs.is_empty());
521    }
522
523    fn has_active_dialog_for_webview(&self, webview_id: WebViewId) -> bool {
524        // First lazily clean up any empty dialog vectors.
525        let mut dialogs = self.dialogs.borrow_mut();
526        dialogs.retain(|_, dialogs| !dialogs.is_empty());
527        dialogs.contains_key(&webview_id)
528    }
529
530    fn toolbar_height(&self) -> Length<f32, DeviceIndependentPixel> {
531        self.gui.borrow().toolbar_height()
532    }
533
534    pub(crate) fn handle_winit_window_event(
535        &self,
536        state: Rc<RunningAppState>,
537        window: Rc<ServoShellWindow>,
538        event: WindowEvent,
539    ) {
540        // Handle resize events first, so that any subsequent redrawing draws onto a buffer of the
541        // correct size.
542        let mut resized = false;
543        if let WindowEvent::Resized(new_inner_size) = event &&
544            self.inner_size.get() != new_inner_size
545        {
546            self.inner_size.set(new_inner_size);
547            self.window_rendering_context.resize(new_inner_size);
548            resized = true;
549        }
550
551        // If requested to redraw or resized, repaint as soon as possible, so that new buffer
552        // contents are available to the window manager.
553        if event == WindowEvent::RedrawRequested || resized {
554            let mut gui = self.gui.borrow_mut();
555            gui.update(&state, &window, self);
556            gui.paint(&self.winit_window);
557        }
558
559        if let WindowEvent::CursorMoved { position, .. } = event {
560            self.last_mouse_position.set(Some(
561                winit_position_to_euclid_point(position).to_f32() / self.hidpi_scale_factor(),
562            ));
563        }
564        let should_forward_mouse_event_to_egui = || {
565            // If a dialog is showing, it always captures all mouse events.
566            if window
567                .active_webview()
568                .is_some_and(|webview| self.has_active_dialog_for_webview(webview.id()))
569            {
570                return true;
571            }
572            // Otherwise, if the cursor is over the egui interface, forward the event.
573            self.last_mouse_position
574                .get()
575                .is_none_or(|point| self.gui.borrow().is_in_egui_toolbar_rect(point))
576        };
577
578        // Handle the event
579        let mut consumed = false;
580        match event {
581            WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
582                // Intercept any ScaleFactorChanged events away from EguiGlow::on_window_event, so
583                // we can use our own logic for calculating the scale factor and set egui’s
584                // scale factor to that value manually.
585                let desired_scale_factor = self.hidpi_scale_factor().get();
586                let effective_egui_zoom_factor = desired_scale_factor / scale_factor as f32;
587
588                info!(
589                    "window scale factor changed to {}, setting egui zoom factor to {}",
590                    scale_factor, effective_egui_zoom_factor
591                );
592
593                self.gui
594                    .borrow()
595                    .set_zoom_factor(effective_egui_zoom_factor);
596
597                window.hidpi_scale_factor_changed();
598
599                // Request a winit redraw event, so we can recomposite, update and paint
600                // the GUI, and present the new frame.
601                self.winit_window.request_redraw();
602            },
603            WindowEvent::MouseInput {
604                state: ElementState::Pressed,
605                button: MouseButton::Forward,
606                ..
607            } => {
608                window.queue_user_interface_command(UserInterfaceCommand::Forward);
609                consumed = true;
610            },
611            WindowEvent::MouseInput {
612                state: ElementState::Pressed,
613                button: MouseButton::Back,
614                ..
615            } => {
616                window.queue_user_interface_command(UserInterfaceCommand::Back);
617                consumed = true;
618            },
619            WindowEvent::MouseWheel { .. } | WindowEvent::MouseInput { .. }
620                if !should_forward_mouse_event_to_egui() =>
621            {
622                self.gui.borrow().surrender_focus();
623            },
624            WindowEvent::KeyboardInput { .. } if !self.gui.borrow().has_keyboard_focus() => {
625                // Keyboard events should go to the WebView unless some other GUI
626                // component has keyboard focus.
627            },
628            ref event => {
629                let response = self
630                    .gui
631                    .borrow_mut()
632                    .on_window_event(&self.winit_window, event);
633
634                if let WindowEvent::Focused(true) = event {
635                    state.handle_focused(window.clone());
636                }
637
638                if response.repaint && *event != WindowEvent::RedrawRequested {
639                    self.winit_window.request_redraw();
640                }
641
642                // All CursorMoved events, even when forwarded to the WebView, are also
643                // forwarded to Gui (above). This is because egui needs to know when
644                // the mouse is moving in other parts of the view in order to properly
645                // hide tooltips.
646                if let WindowEvent::CursorMoved { .. } = event &&
647                    !should_forward_mouse_event_to_egui()
648                {
649                    consumed = false;
650                } else {
651                    // TODO how do we handle the tab key? (see doc for consumed)
652                    // Note that servo doesn’t yet support tabbing through links and inputs
653                    consumed = response.consumed;
654                }
655            },
656        }
657
658        if !consumed && let Some(webview) = window.active_webview() {
659            match event {
660                WindowEvent::KeyboardInput { event, .. } => {
661                    self.handle_keyboard_input(state, &window, event)
662                },
663                WindowEvent::ModifiersChanged(modifiers) => {
664                    self.modifiers_state.set(modifiers.state())
665                },
666                WindowEvent::MouseInput { state, button, .. } => {
667                    self.handle_mouse_button_event(&webview, button, state);
668                },
669                WindowEvent::CursorMoved { position, .. } => {
670                    self.handle_mouse_move_event(&webview, position);
671                },
672                WindowEvent::CursorLeft { .. } => {
673                    let webview_rect: Rect<_, _> = webview.size().into();
674                    if webview_rect.contains(self.webview_relative_mouse_point.get()) {
675                        webview.notify_input_event(InputEvent::MouseLeftViewport(
676                            MouseLeftViewportEvent::default(),
677                        ));
678                    }
679                },
680                WindowEvent::MouseWheel { delta, .. } => {
681                    let (delta_x, delta_y, mode) = match delta {
682                        MouseScrollDelta::LineDelta(delta_x, delta_y) => (
683                            (delta_x * LINE_WIDTH) as f64,
684                            (delta_y * LINE_HEIGHT) as f64,
685                            WheelMode::DeltaPixel,
686                        ),
687                        MouseScrollDelta::PixelDelta(delta) => {
688                            (delta.x, delta.y, WheelMode::DeltaPixel)
689                        },
690                    };
691
692                    // Create wheel event before snapping to the major axis of movement
693                    let delta = WheelDelta {
694                        x: delta_x,
695                        y: delta_y,
696                        z: 0.0,
697                        mode,
698                    };
699                    let point = self.webview_relative_mouse_point.get();
700                    webview.notify_input_event(InputEvent::Wheel(WheelEvent::new(
701                        delta,
702                        point.into(),
703                    )));
704                },
705                WindowEvent::Touch(touch) => {
706                    webview.notify_input_event(InputEvent::Touch(TouchEvent::new(
707                        winit_phase_to_touch_event_type(touch.phase),
708                        TouchId(touch.id as i32),
709                        DevicePoint::new(touch.location.x as f32, touch.location.y as f32).into(),
710                        TouchPointerType::Touch,
711                    )));
712                },
713                WindowEvent::PinchGesture { delta, .. } => {
714                    webview.adjust_pinch_zoom(
715                        delta as f32 + 1.0,
716                        self.webview_relative_mouse_point.get(),
717                    );
718                },
719                WindowEvent::CloseRequested => {
720                    window.schedule_close();
721                },
722                WindowEvent::ThemeChanged(theme) => {
723                    webview.notify_theme_change(match theme {
724                        winit::window::Theme::Light => Theme::Light,
725                        winit::window::Theme::Dark => Theme::Dark,
726                    });
727                },
728                WindowEvent::Ime(ime) => match ime {
729                    Ime::Enabled => {
730                        webview.notify_input_event(InputEvent::Ime(ImeEvent::Composition(
731                            servo::CompositionEvent {
732                                state: servo::CompositionState::Start,
733                                data: String::new(),
734                            },
735                        )));
736                    },
737                    Ime::Preedit(text, _) => {
738                        webview.notify_input_event(InputEvent::Ime(ImeEvent::Composition(
739                            servo::CompositionEvent {
740                                state: servo::CompositionState::Update,
741                                data: text,
742                            },
743                        )));
744                    },
745                    Ime::Commit(text) => {
746                        webview.notify_input_event(InputEvent::Ime(ImeEvent::Composition(
747                            servo::CompositionEvent {
748                                state: servo::CompositionState::End,
749                                data: text,
750                            },
751                        )));
752                    },
753                    Ime::Disabled => {
754                        // There are two reasons we receive this message from winit:
755                        //
756                        // 1. The user dismissed the IME. In that case we want to inform Servo
757                        //    so it can unfocus the current editable element.
758                        // 2. Servo changed focus and requested that we dismiss the IME, which
759                        //    in turn triggers this message. We know this is the case when we don't
760                        //    expect any IME to be open and shouldn't send any more messages to
761                        //    Servo as it might cause unexpected blurring of the newly focused
762                        //    element.
763                        if self.visible_input_method.take().is_some() {
764                            webview.notify_input_event(InputEvent::Ime(ImeEvent::Dismissed));
765                        }
766                    },
767                },
768                WindowEvent::DroppedFile(dropped_file) => {
769                    if let Ok(url) = Url::from_file_path(&dropped_file) {
770                        webview.load(url);
771                    } else {
772                        log::error!(
773                            "Failed to create URL for dropped file ({})",
774                            dropped_file.display()
775                        );
776                    }
777                },
778                _ => {},
779            }
780        }
781    }
782
783    pub(crate) fn handle_winit_app_event(&self, state: Rc<RunningAppState>, app_event: AppEvent) {
784        if let AppEvent::Accessibility(ref event) = app_event {
785            match &event.window_event {
786                egui_winit::accesskit_winit::WindowEvent::InitialTreeRequested => {
787                    state.set_accessibility_active(true);
788                },
789                egui_winit::accesskit_winit::WindowEvent::ActionRequested(req) => {
790                    if req.target_tree != accesskit::TreeId::ROOT {
791                        // TODO(#4344): Forward action to Servo
792                    }
793                },
794                egui_winit::accesskit_winit::WindowEvent::AccessibilityDeactivated => {
795                    state.set_accessibility_active(false);
796                },
797            }
798
799            if self
800                .gui
801                .borrow_mut()
802                .handle_accesskit_event(&event.window_event)
803            {
804                self.winit_window.request_redraw();
805            }
806        }
807    }
808}
809
810impl PlatformWindow for HeadedWindow {
811    fn as_headed_window(&self) -> Option<&Self> {
812        Some(self)
813    }
814
815    fn screen_geometry(&self) -> ScreenGeometry {
816        let hidpi_factor = self.hidpi_scale_factor();
817        let toolbar_size = Size2D::new(0.0, (self.toolbar_height() * self.hidpi_scale_factor()).0);
818        let screen_size = self.screen_size.to_f32() * hidpi_factor;
819
820        // FIXME: In reality, this should subtract screen space used by the system interface
821        // elements, but it is difficult to get this value with `winit` currently. See:
822        // See https://github.com/rust-windowing/winit/issues/2494
823        let available_screen_size = screen_size - toolbar_size;
824
825        let window_rect = DeviceIntRect::from_origin_and_size(
826            winit_position_to_euclid_point(self.winit_window.outer_position().unwrap_or_default()),
827            winit_size_to_euclid_size(self.winit_window.outer_size()).to_i32(),
828        );
829
830        ScreenGeometry {
831            size: screen_size.to_i32(),
832            available_size: available_screen_size.to_i32(),
833            window_rect,
834        }
835    }
836
837    fn device_hidpi_scale_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel> {
838        Scale::new(self.winit_window.scale_factor() as f32)
839    }
840
841    fn hidpi_scale_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel> {
842        self.device_pixel_ratio_override
843            .map(Scale::new)
844            .unwrap_or_else(|| self.device_hidpi_scale_factor())
845    }
846
847    fn update_user_interface_state(&self, _: &RunningAppState, window: &ServoShellWindow) -> bool {
848        let title = window
849            .active_webview()
850            .and_then(|webview| {
851                webview
852                    .page_title()
853                    .filter(|title| !title.is_empty())
854                    .or_else(|| webview.url().map(|url| url.to_string()))
855            })
856            .unwrap_or_else(|| INITIAL_WINDOW_TITLE.to_string());
857        if title != *self.last_title.borrow() {
858            self.winit_window.set_title(&title);
859            *self.last_title.borrow_mut() = title;
860        }
861
862        self.gui.borrow_mut().update_webview_data(window)
863    }
864
865    fn request_repaint(&self, _: &ServoShellWindow) {
866        self.winit_window.request_redraw();
867    }
868
869    fn request_resize(&self, _: &WebView, new_outer_size: DeviceIntSize) -> Option<DeviceIntSize> {
870        // Allocate space for the window deocrations, but do not let the inner size get
871        // smaller than `MIN_WINDOW_INNER_SIZE` or larger than twice the screen size.
872        let inner_size = self.winit_window.inner_size();
873        let outer_size = self.winit_window.outer_size();
874        let decoration_size: DeviceIntSize = Size2D::new(
875            outer_size.height - inner_size.height,
876            outer_size.width - inner_size.width,
877        )
878        .cast();
879
880        let screen_size = (self.screen_size.to_f32() * self.hidpi_scale_factor()).to_i32();
881        let new_outer_size =
882            new_outer_size.clamp(MIN_WINDOW_INNER_SIZE + decoration_size, screen_size * 2);
883
884        if outer_size.width == new_outer_size.width as u32 &&
885            outer_size.height == new_outer_size.height as u32
886        {
887            return Some(new_outer_size);
888        }
889
890        let new_inner_size = new_outer_size - decoration_size;
891        self.winit_window
892            .request_inner_size(PhysicalSize::new(
893                new_inner_size.width,
894                new_inner_size.height,
895            ))
896            .map(|resulting_size| {
897                DeviceIntSize::new(
898                    resulting_size.width as i32 + decoration_size.width,
899                    resulting_size.height as i32 + decoration_size.height,
900                )
901            })
902    }
903
904    fn window_rect(&self) -> DeviceIndependentIntRect {
905        let outer_size = self.winit_window.outer_size();
906        let scale = self.hidpi_scale_factor();
907
908        let outer_size = winit_size_to_euclid_size(outer_size).to_i32();
909
910        let origin = self
911            .winit_window
912            .outer_position()
913            .map(winit_position_to_euclid_point)
914            .unwrap_or_default();
915        convert_rect_to_css_pixel(
916            DeviceIntRect::from_origin_and_size(origin, outer_size),
917            scale,
918        )
919    }
920
921    fn set_position(&self, point: DeviceIntPoint) {
922        self.winit_window
923            .set_outer_position::<PhysicalPosition<i32>>(PhysicalPosition::new(point.x, point.y))
924    }
925
926    fn set_fullscreen(&self, state: bool) {
927        let monitor = self
928            .winit_window()
929            .current_monitor()
930            .or_else(|| self.winit_window.available_monitors().nth(0))
931            .expect("No monitor detected");
932        if self.fullscreen.get() != state {
933            self.winit_window.set_fullscreen(if state {
934                Some(winit::window::Fullscreen::Borderless(Some(monitor)))
935            } else {
936                None
937            });
938        }
939        self.fullscreen.set(state);
940    }
941
942    fn get_fullscreen(&self) -> bool {
943        self.fullscreen.get()
944    }
945
946    fn set_cursor(&self, cursor: Cursor) {
947        use winit::window::CursorIcon;
948
949        let winit_cursor = match cursor {
950            Cursor::Default => CursorIcon::Default,
951            Cursor::Pointer => CursorIcon::Pointer,
952            Cursor::ContextMenu => CursorIcon::ContextMenu,
953            Cursor::Help => CursorIcon::Help,
954            Cursor::Progress => CursorIcon::Progress,
955            Cursor::Wait => CursorIcon::Wait,
956            Cursor::Cell => CursorIcon::Cell,
957            Cursor::Crosshair => CursorIcon::Crosshair,
958            Cursor::Text => CursorIcon::Text,
959            Cursor::VerticalText => CursorIcon::VerticalText,
960            Cursor::Alias => CursorIcon::Alias,
961            Cursor::Copy => CursorIcon::Copy,
962            Cursor::Move => CursorIcon::Move,
963            Cursor::NoDrop => CursorIcon::NoDrop,
964            Cursor::NotAllowed => CursorIcon::NotAllowed,
965            Cursor::Grab => CursorIcon::Grab,
966            Cursor::Grabbing => CursorIcon::Grabbing,
967            Cursor::EResize => CursorIcon::EResize,
968            Cursor::NResize => CursorIcon::NResize,
969            Cursor::NeResize => CursorIcon::NeResize,
970            Cursor::NwResize => CursorIcon::NwResize,
971            Cursor::SResize => CursorIcon::SResize,
972            Cursor::SeResize => CursorIcon::SeResize,
973            Cursor::SwResize => CursorIcon::SwResize,
974            Cursor::WResize => CursorIcon::WResize,
975            Cursor::EwResize => CursorIcon::EwResize,
976            Cursor::NsResize => CursorIcon::NsResize,
977            Cursor::NeswResize => CursorIcon::NeswResize,
978            Cursor::NwseResize => CursorIcon::NwseResize,
979            Cursor::ColResize => CursorIcon::ColResize,
980            Cursor::RowResize => CursorIcon::RowResize,
981            Cursor::AllScroll => CursorIcon::AllScroll,
982            Cursor::ZoomIn => CursorIcon::ZoomIn,
983            Cursor::ZoomOut => CursorIcon::ZoomOut,
984            Cursor::None => {
985                self.winit_window.set_cursor_visible(false);
986                return;
987            },
988        };
989        self.winit_window.set_cursor(winit_cursor);
990        self.winit_window.set_cursor_visible(true);
991    }
992
993    fn id(&self) -> ServoShellWindowId {
994        let id: u64 = self.winit_window.id().into();
995        id.into()
996    }
997
998    #[cfg(feature = "webxr")]
999    fn new_glwindow(&self, event_loop: &ActiveEventLoop) -> Rc<dyn servo::webxr::GlWindow> {
1000        let size = self.winit_window.outer_size();
1001
1002        let window_attr = winit::window::Window::default_attributes()
1003            .with_title("Servo XR".to_string())
1004            .with_inner_size(size)
1005            .with_visible(false);
1006
1007        let winit_window = event_loop
1008            .create_window(window_attr)
1009            .expect("Failed to create window.");
1010
1011        let pose = Rc::new(XRWindowPose {
1012            xr_rotation: Cell::new(Rotation3D::identity()),
1013            xr_translation: Cell::new(Vector3D::zero()),
1014        });
1015        self.xr_window_poses.borrow_mut().push(pose.clone());
1016        Rc::new(XRWindow { winit_window, pose })
1017    }
1018
1019    fn rendering_context(&self) -> Rc<dyn RenderingContext> {
1020        self.rendering_context.clone()
1021    }
1022
1023    fn theme(&self) -> servo::Theme {
1024        match self.winit_window.theme() {
1025            Some(winit::window::Theme::Dark) => servo::Theme::Dark,
1026            Some(winit::window::Theme::Light) | None => servo::Theme::Light,
1027        }
1028    }
1029
1030    fn maximize(&self, _webview: &WebView) {
1031        self.winit_window.set_maximized(true);
1032    }
1033
1034    /// Handle servoshell key bindings that may have been prevented by the page in the active webview.
1035    fn notify_input_event_handled(
1036        &self,
1037        webview: &WebView,
1038        id: InputEventId,
1039        result: InputEventResult,
1040    ) {
1041        let Some(keyboard_event) = self.pending_keyboard_events.borrow_mut().remove(&id) else {
1042            return;
1043        };
1044        if result.intersects(InputEventResult::DefaultPrevented | InputEventResult::Consumed) {
1045            return;
1046        }
1047
1048        ShortcutMatcher::from_event(keyboard_event.event)
1049            .shortcut(CMD_OR_CONTROL, '=', || {
1050                webview.set_page_zoom(webview.page_zoom() + 0.1);
1051            })
1052            .shortcut(CMD_OR_CONTROL, '+', || {
1053                webview.set_page_zoom(webview.page_zoom() + 0.1);
1054            })
1055            .shortcut(CMD_OR_CONTROL, '-', || {
1056                webview.set_page_zoom(webview.page_zoom() - 0.1);
1057            })
1058            .shortcut(CMD_OR_CONTROL, '0', || {
1059                webview.set_page_zoom(1.0);
1060            })
1061            .shortcut(CMD_OR_CONTROL, 'R', || webview.reload())
1062            .shortcut(Modifiers::empty(), Key::Named(NamedKey::F5), || {
1063                webview.reload()
1064            });
1065    }
1066
1067    fn focus(&self) {
1068        self.winit_window.focus_window();
1069    }
1070
1071    fn has_platform_focus(&self) -> bool {
1072        self.winit_window.has_focus()
1073    }
1074
1075    fn show_embedder_control(&self, webview_id: WebViewId, embedder_control: EmbedderControl) {
1076        let control_id = embedder_control.id();
1077        match embedder_control {
1078            EmbedderControl::SelectElement(prompt) => {
1079                // FIXME: Reading the toolbar height is needed here to properly position the select dialog.
1080                // But if the toolbar height changes while the dialog is open then the position won't be updated
1081                let offset = self.gui.borrow().toolbar_height();
1082                self.add_dialog(
1083                    webview_id,
1084                    Dialog::new_select_element_dialog(prompt, offset),
1085                );
1086            },
1087            EmbedderControl::ColorPicker(color_picker) => {
1088                // FIXME: Reading the toolbar height is needed here to properly position the select dialog.
1089                // But if the toolbar height changes while the dialog is open then the position won't be updated
1090                let offset = self.gui.borrow().toolbar_height();
1091                self.add_dialog(
1092                    webview_id,
1093                    Dialog::new_color_picker_dialog(color_picker, offset),
1094                );
1095            },
1096            EmbedderControl::InputMethod(input_method_control) => {
1097                self.show_ime(control_id, input_method_control);
1098            },
1099            EmbedderControl::FilePicker(file_picker) => {
1100                self.add_dialog(webview_id, Dialog::new_file_dialog(file_picker));
1101            },
1102            EmbedderControl::SimpleDialog(simple_dialog) => {
1103                self.add_dialog(webview_id, Dialog::new_simple_dialog(simple_dialog));
1104            },
1105            EmbedderControl::ContextMenu(prompt) => {
1106                let offset = self.gui.borrow().toolbar_height();
1107                self.add_dialog(webview_id, Dialog::new_context_menu(prompt, offset));
1108            },
1109        }
1110    }
1111
1112    fn hide_embedder_control(&self, webview_id: WebViewId, embedder_control_id: EmbedderControlId) {
1113        if self.visible_input_method.get() == Some(embedder_control_id) {
1114            self.visible_input_method.set(None);
1115            self.winit_window.set_ime_allowed(false);
1116            return;
1117        }
1118        self.remove_dialog(webview_id, embedder_control_id);
1119    }
1120
1121    fn show_bluetooth_device_dialog(
1122        &self,
1123        webview_id: WebViewId,
1124        request: BluetoothDeviceSelectionRequest,
1125    ) {
1126        self.add_dialog(webview_id, Dialog::new_device_selection_dialog(request));
1127    }
1128
1129    fn show_permission_dialog(&self, webview_id: WebViewId, permission_request: PermissionRequest) {
1130        self.add_dialog(
1131            webview_id,
1132            Dialog::new_permission_request_dialog(permission_request),
1133        );
1134    }
1135
1136    fn show_http_authentication_dialog(
1137        &self,
1138        webview_id: WebViewId,
1139        authentication_request: AuthenticationRequest,
1140    ) {
1141        self.add_dialog(
1142            webview_id,
1143            Dialog::new_authentication_dialog(authentication_request),
1144        );
1145    }
1146
1147    fn dismiss_embedder_controls_for_webview(&self, webview_id: WebViewId) {
1148        self.dialogs.borrow_mut().remove(&webview_id);
1149    }
1150
1151    fn show_console_message(&self, level: servo::ConsoleLogLevel, message: &str) {
1152        println!("{message}");
1153        log::log!(level.into(), "{message}");
1154    }
1155
1156    fn notify_accessibility_tree_update(
1157        &self,
1158        _webview: WebView,
1159        tree_update: accesskit::TreeUpdate,
1160    ) {
1161        self.gui
1162            .borrow_mut()
1163            .notify_accessibility_tree_update(tree_update);
1164    }
1165}
1166
1167fn winit_phase_to_touch_event_type(phase: TouchPhase) -> TouchEventType {
1168    match phase {
1169        TouchPhase::Started => TouchEventType::Down,
1170        TouchPhase::Moved => TouchEventType::Move,
1171        TouchPhase::Ended => TouchEventType::Up,
1172        TouchPhase::Cancelled => TouchEventType::Cancel,
1173    }
1174}
1175
1176#[cfg(any(target_os = "linux", target_os = "windows"))]
1177fn load_icon(icon_bytes: &[u8]) -> Icon {
1178    let (icon_rgba, icon_width, icon_height) = {
1179        use image::{GenericImageView, Pixel};
1180        let image = image::load_from_memory(icon_bytes).expect("Failed to load icon");
1181        let (width, height) = image.dimensions();
1182        let mut rgba = Vec::with_capacity((width * height) as usize * 4);
1183        for (_, _, pixel) in image.pixels() {
1184            rgba.extend_from_slice(&pixel.to_rgba().0);
1185        }
1186        (rgba, width, height)
1187    };
1188    Icon::from_rgba(icon_rgba, icon_width, icon_height).expect("Failed to load icon")
1189}
1190
1191#[cfg(feature = "webxr")]
1192struct XRWindow {
1193    winit_window: winit::window::Window,
1194    pose: Rc<XRWindowPose>,
1195}
1196
1197struct XRWindowPose {
1198    xr_rotation: Cell<Rotation3D<f32, UnknownUnit, UnknownUnit>>,
1199    xr_translation: Cell<Vector3D<f32, UnknownUnit>>,
1200}
1201
1202#[cfg(feature = "webxr")]
1203impl servo::webxr::GlWindow for XRWindow {
1204    fn get_render_target(
1205        &self,
1206        device: &mut surfman::Device,
1207        _context: &mut surfman::Context,
1208    ) -> servo::webxr::GlWindowRenderTarget {
1209        self.winit_window.set_visible(true);
1210        let window_handle = self
1211            .winit_window
1212            .window_handle()
1213            .expect("could not get window handle from window");
1214        let size = self.winit_window.inner_size();
1215        let size = Size2D::new(size.width as i32, size.height as i32);
1216        let native_widget = device
1217            .connection()
1218            .create_native_widget_from_window_handle(window_handle, size)
1219            .expect("Failed to create native widget");
1220        servo::webxr::GlWindowRenderTarget::NativeWidget(native_widget)
1221    }
1222
1223    fn get_rotation(&self) -> Rotation3D<f32, UnknownUnit, UnknownUnit> {
1224        self.pose.xr_rotation.get()
1225    }
1226
1227    fn get_translation(&self) -> Vector3D<f32, UnknownUnit> {
1228        self.pose.xr_translation.get()
1229    }
1230
1231    fn get_mode(&self) -> servo::webxr::GlWindowMode {
1232        use servo::pref;
1233        if pref!(dom_webxr_glwindow_red_cyan) {
1234            servo::webxr::GlWindowMode::StereoRedCyan
1235        } else if pref!(dom_webxr_glwindow_left_right) {
1236            servo::webxr::GlWindowMode::StereoLeftRight
1237        } else if pref!(dom_webxr_glwindow_spherical) {
1238            servo::webxr::GlWindowMode::Spherical
1239        } else if pref!(dom_webxr_glwindow_cubemap) {
1240            servo::webxr::GlWindowMode::Cubemap
1241        } else {
1242            servo::webxr::GlWindowMode::Blit
1243        }
1244    }
1245
1246    fn display_handle(&self) -> raw_window_handle::DisplayHandle<'_> {
1247        self.winit_window
1248            .display_handle()
1249            .expect("Every window should have a display handle")
1250    }
1251}
1252
1253impl XRWindowPose {
1254    fn handle_xr_translation(&self, input: &KeyboardEvent) {
1255        if input.event.state != KeyState::Down {
1256            return;
1257        }
1258        const NORMAL_TRANSLATE: f32 = 0.1;
1259        const QUICK_TRANSLATE: f32 = 1.0;
1260        let mut x = 0.0;
1261        let mut z = 0.0;
1262        match input.event.key {
1263            Key::Character(ref k) => match &**k {
1264                "w" => z = -NORMAL_TRANSLATE,
1265                "W" => z = -QUICK_TRANSLATE,
1266                "s" => z = NORMAL_TRANSLATE,
1267                "S" => z = QUICK_TRANSLATE,
1268                "a" => x = -NORMAL_TRANSLATE,
1269                "A" => x = -QUICK_TRANSLATE,
1270                "d" => x = NORMAL_TRANSLATE,
1271                "D" => x = QUICK_TRANSLATE,
1272                _ => return,
1273            },
1274            _ => return,
1275        };
1276        let (old_x, old_y, old_z) = self.xr_translation.get().to_tuple();
1277        let vec = Vector3D::new(x + old_x, old_y, z + old_z);
1278        self.xr_translation.set(vec);
1279    }
1280
1281    fn handle_xr_rotation(&self, input: &KeyEvent, modifiers: ModifiersState) {
1282        if input.state != ElementState::Pressed {
1283            return;
1284        }
1285        let mut x = 0.0;
1286        let mut y = 0.0;
1287        match input.logical_key {
1288            LogicalKey::Named(WinitNamedKey::ArrowUp) => x = 1.0,
1289            LogicalKey::Named(WinitNamedKey::ArrowDown) => x = -1.0,
1290            LogicalKey::Named(WinitNamedKey::ArrowLeft) => y = 1.0,
1291            LogicalKey::Named(WinitNamedKey::ArrowRight) => y = -1.0,
1292            _ => return,
1293        };
1294        if modifiers.shift_key() {
1295            x *= 10.0;
1296            y *= 10.0;
1297        }
1298        let x: Rotation3D<_, UnknownUnit, UnknownUnit> = Rotation3D::around_x(Angle::degrees(x));
1299        let y: Rotation3D<_, UnknownUnit, UnknownUnit> = Rotation3D::around_y(Angle::degrees(y));
1300        let rotation = self.xr_rotation.get().then(&x).then(&y);
1301        self.xr_rotation.set(rotation);
1302    }
1303}
1304
1305#[derive(Default)]
1306pub struct TouchEventSimulator {
1307    pub left_mouse_button_down: Cell<bool>,
1308}
1309
1310impl TouchEventSimulator {
1311    fn maybe_consume_move_button_event(
1312        &self,
1313        webview: &WebView,
1314        button: MouseButton,
1315        action: ElementState,
1316        point: DevicePoint,
1317    ) -> bool {
1318        if button != MouseButton::Left {
1319            return false;
1320        }
1321
1322        if action == ElementState::Pressed && !self.left_mouse_button_down.get() {
1323            webview.notify_input_event(InputEvent::Touch(TouchEvent::new(
1324                TouchEventType::Down,
1325                TouchId(0),
1326                point.into(),
1327                TouchPointerType::Touch,
1328            )));
1329            self.left_mouse_button_down.set(true);
1330        } else if action == ElementState::Released {
1331            webview.notify_input_event(InputEvent::Touch(TouchEvent::new(
1332                TouchEventType::Up,
1333                TouchId(0),
1334                point.into(),
1335                TouchPointerType::Touch,
1336            )));
1337            self.left_mouse_button_down.set(false);
1338        }
1339
1340        true
1341    }
1342
1343    fn maybe_consume_mouse_move_event(
1344        &self,
1345        webview: &WebView,
1346        point: Point2D<f32, DevicePixel>,
1347    ) -> bool {
1348        if !self.left_mouse_button_down.get() {
1349            return false;
1350        }
1351
1352        webview.notify_input_event(InputEvent::Touch(TouchEvent::new(
1353            TouchEventType::Move,
1354            TouchId(0),
1355            point.into(),
1356            TouchPointerType::Touch,
1357        )));
1358        true
1359    }
1360}