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, Rotation3D, Scale, Size2D, UnknownUnit, Vector3D};
17use keyboard_types::ShortcutMatcher;
18use log::debug;
19use raw_window_handle::{HasDisplayHandle, HasWindowHandle, RawWindowHandle};
20use servo::servo_geometry::{
21    DeviceIndependentIntRect, DeviceIndependentPixel, convert_rect_to_css_pixel,
22};
23use servo::webrender_api::units::{
24    DeviceIntPoint, DeviceIntRect, DeviceIntSize, DevicePixel, DevicePoint,
25};
26use servo::{
27    Cursor, ImeEvent, InputEvent, InputEventId, InputEventResult, InputMethodControl, Key,
28    KeyState, KeyboardEvent, Modifiers, MouseButton as ServoMouseButton, MouseButtonAction,
29    MouseButtonEvent, MouseLeftViewportEvent, MouseMoveEvent, NamedKey, OffscreenRenderingContext,
30    RenderingContext, ScreenGeometry, Theme, TouchEvent, TouchEventType, TouchId,
31    WebRenderDebugOption, WebView, WheelDelta, WheelEvent, WheelMode, WindowRenderingContext,
32};
33use url::Url;
34use winit::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize};
35use winit::event::{
36    ElementState, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase, WindowEvent,
37};
38use winit::event_loop::ActiveEventLoop;
39use winit::keyboard::{Key as LogicalKey, ModifiersState, NamedKey as WinitNamedKey};
40#[cfg(target_os = "linux")]
41use winit::platform::wayland::WindowAttributesExtWayland;
42#[cfg(any(target_os = "linux", target_os = "windows"))]
43use winit::window::Icon;
44#[cfg(target_os = "macos")]
45use {
46    objc2_app_kit::{NSColorSpace, NSView},
47    objc2_foundation::MainThreadMarker,
48};
49
50use super::app_state::RunningAppState;
51use super::geometry::{winit_position_to_euclid_point, winit_size_to_euclid_size};
52use super::keyutils::{CMD_OR_ALT, keyboard_event_from_winit};
53use super::window_trait::{LINE_HEIGHT, LINE_WIDTH, WindowPortsMethods};
54use crate::desktop::accelerated_gl_media::setup_gl_accelerated_media;
55use crate::desktop::keyutils::CMD_OR_CONTROL;
56use crate::desktop::window_trait::MIN_WINDOW_INNER_SIZE;
57use crate::prefs::ServoShellPreferences;
58use crate::running_app_state::RunningAppStateTrait;
59
60pub(crate) const INITIAL_WINDOW_TITLE: &str = "Servo";
61
62pub struct Window {
63    screen_size: Size2D<u32, DeviceIndependentPixel>,
64    toolbar_height: Cell<Length<f32, DeviceIndependentPixel>>,
65    monitor: winit::monitor::MonitorHandle,
66    webview_relative_mouse_point: Cell<Point2D<f32, DevicePixel>>,
67    /// The inner size of the window in physical pixels which excludes OS decorations.
68    /// It equals viewport size + (0, toolbar height).
69    inner_size: Cell<PhysicalSize<u32>>,
70    fullscreen: Cell<bool>,
71    device_pixel_ratio_override: Option<f32>,
72    xr_window_poses: RefCell<Vec<Rc<XRWindowPose>>>,
73    modifiers_state: Cell<ModifiersState>,
74
75    /// The `RenderingContext` of Servo itself. This is used to render Servo results
76    /// temporarily until they can be blitted into the egui scene.
77    rendering_context: Rc<OffscreenRenderingContext>,
78    /// The RenderingContext that renders directly onto the Window. This is used as
79    /// the target of egui rendering and also where Servo rendering results are finally
80    /// blitted.
81    window_rendering_context: Rc<WindowRenderingContext>,
82    /// A helper that simulates touch events when the `--simulate-touch-events` flag
83    /// is enabled.
84    touch_event_simulator: Option<TouchEventSimulator>,
85    /// Keyboard events that have been sent to Servo that have still not been handled yet.
86    /// When these are handled, they will optionally be used to trigger keybindings that
87    /// are overridable by web content.
88    pending_keyboard_events: RefCell<HashMap<InputEventId, KeyboardEvent>>,
89
90    // Keep this as the last field of the struct to ensure that the rendering context is
91    // dropped first.
92    // (https://github.com/servo/servo/issues/36711)
93    winit_window: winit::window::Window,
94
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}
99
100impl Window {
101    pub fn new(
102        servoshell_preferences: &ServoShellPreferences,
103        event_loop: &ActiveEventLoop,
104    ) -> Window {
105        let no_native_titlebar = servoshell_preferences.no_native_titlebar;
106        let inner_size = servoshell_preferences.initial_window_size;
107        let window_attr = winit::window::Window::default_attributes()
108            .with_title(INITIAL_WINDOW_TITLE.to_string())
109            .with_decorations(!no_native_titlebar)
110            .with_transparent(no_native_titlebar)
111            .with_inner_size(LogicalSize::new(inner_size.width, inner_size.height))
112            .with_min_inner_size(LogicalSize::new(
113                MIN_WINDOW_INNER_SIZE.width,
114                MIN_WINDOW_INNER_SIZE.height,
115            ))
116            // Must be invisible at startup; accesskit_winit setup needs to
117            // happen before the window is shown for the first time.
118            .with_visible(false);
119
120        // Set a name so it can be pinned to taskbars in Linux.
121        #[cfg(target_os = "linux")]
122        let window_attr = window_attr.with_name("org.servo.Servo", "Servo");
123
124        #[allow(deprecated)]
125        let winit_window = event_loop
126            .create_window(window_attr)
127            .expect("Failed to create window.");
128
129        #[cfg(any(target_os = "linux", target_os = "windows"))]
130        {
131            let icon_bytes = include_bytes!("../../../resources/servo_64.png");
132            winit_window.set_window_icon(Some(load_icon(icon_bytes)));
133        }
134
135        let window_handle = winit_window
136            .window_handle()
137            .expect("winit window did not have a window handle");
138        Window::force_srgb_color_space(window_handle.as_raw());
139
140        let monitor = winit_window
141            .current_monitor()
142            .or_else(|| winit_window.available_monitors().nth(0))
143            .expect("No monitor detected");
144
145        let (screen_size, screen_scale) = servoshell_preferences.screen_size_override.map_or_else(
146            || (monitor.size(), winit_window.scale_factor()),
147            |size| (PhysicalSize::new(size.width, size.height), 1.0),
148        );
149        let screen_scale: Scale<f64, DeviceIndependentPixel, DevicePixel> =
150            Scale::new(screen_scale);
151        let screen_size = (winit_size_to_euclid_size(screen_size).to_f64() / screen_scale).to_u32();
152        let inner_size = winit_window.inner_size();
153
154        let display_handle = event_loop
155            .display_handle()
156            .expect("could not get display handle from window");
157        let window_handle = winit_window
158            .window_handle()
159            .expect("could not get window handle from window");
160        let window_rendering_context = Rc::new(
161            WindowRenderingContext::new(display_handle, window_handle, inner_size)
162                .expect("Could not create RenderingContext for Window"),
163        );
164
165        // Setup for GL accelerated media handling. This is only active on certain Linux platforms
166        // and Windows.
167        {
168            let details = window_rendering_context.surfman_details();
169            setup_gl_accelerated_media(details.0, details.1);
170        }
171
172        // Make sure the gl context is made current.
173        window_rendering_context
174            .make_current()
175            .expect("Could not make window RenderingContext current");
176
177        let rendering_context = Rc::new(window_rendering_context.offscreen_context(inner_size));
178
179        debug!("Created window {:?}", winit_window.id());
180        Window {
181            winit_window,
182            webview_relative_mouse_point: Cell::new(Point2D::zero()),
183            fullscreen: Cell::new(false),
184            inner_size: Cell::new(inner_size),
185            monitor,
186            screen_size,
187            device_pixel_ratio_override: servoshell_preferences.device_pixel_ratio_override,
188            xr_window_poses: RefCell::new(vec![]),
189            modifiers_state: Cell::new(ModifiersState::empty()),
190            toolbar_height: Cell::new(Default::default()),
191            window_rendering_context,
192            touch_event_simulator: servoshell_preferences
193                .simulate_touch_events
194                .then(Default::default),
195            pending_keyboard_events: Default::default(),
196            rendering_context,
197            last_title: RefCell::new(String::from(INITIAL_WINDOW_TITLE)),
198        }
199    }
200
201    fn handle_keyboard_input(&self, state: Rc<RunningAppState>, winit_event: KeyEvent) {
202        // First, handle servoshell key bindings that are not overridable by, or visible to, the page.
203        let keyboard_event = keyboard_event_from_winit(&winit_event, self.modifiers_state.get());
204        if self.handle_intercepted_key_bindings(state.clone(), &keyboard_event) {
205            return;
206        }
207
208        // Then we deliver character and keyboard events to the page in the focused webview.
209        let Some(webview) = state.focused_webview() else {
210            return;
211        };
212
213        for xr_window_pose in self.xr_window_poses.borrow().iter() {
214            xr_window_pose.handle_xr_rotation(&winit_event, self.modifiers_state.get());
215            xr_window_pose.handle_xr_translation(&keyboard_event);
216        }
217
218        let id = webview.notify_input_event(InputEvent::Keyboard(keyboard_event.clone()));
219        self.pending_keyboard_events
220            .borrow_mut()
221            .insert(id, keyboard_event);
222    }
223
224    /// Helper function to handle a click
225    fn handle_mouse_button_event(
226        &self,
227        webview: &WebView,
228        button: MouseButton,
229        action: ElementState,
230    ) {
231        // `point` can be outside viewport, such as at toolbar with negative y-coordinate.
232        let point = self.webview_relative_mouse_point.get();
233        if !webview.rect().contains(point) {
234            return;
235        }
236
237        if self
238            .touch_event_simulator
239            .as_ref()
240            .is_some_and(|touch_event_simulator| {
241                touch_event_simulator
242                    .maybe_consume_move_button_event(webview, button, action, point)
243            })
244        {
245            return;
246        }
247
248        let mouse_button = match &button {
249            MouseButton::Left => ServoMouseButton::Left,
250            MouseButton::Right => ServoMouseButton::Right,
251            MouseButton::Middle => ServoMouseButton::Middle,
252            MouseButton::Back => ServoMouseButton::Back,
253            MouseButton::Forward => ServoMouseButton::Forward,
254            MouseButton::Other(value) => ServoMouseButton::Other(*value),
255        };
256
257        let action = match action {
258            ElementState::Pressed => MouseButtonAction::Down,
259            ElementState::Released => MouseButtonAction::Up,
260        };
261
262        webview.notify_input_event(InputEvent::MouseButton(MouseButtonEvent::new(
263            action,
264            mouse_button,
265            point.into(),
266        )));
267    }
268
269    /// Helper function to handle mouse move events.
270    fn handle_mouse_move_event(&self, webview: &WebView, position: PhysicalPosition<f64>) {
271        let mut point = winit_position_to_euclid_point(position).to_f32();
272        point.y -= (self.toolbar_height() * self.hidpi_scale_factor()).0;
273
274        let previous_point = self.webview_relative_mouse_point.get();
275        self.webview_relative_mouse_point.set(point);
276
277        if !webview.rect().contains(point) {
278            if webview.rect().contains(previous_point) {
279                webview.notify_input_event(InputEvent::MouseLeftViewport(
280                    MouseLeftViewportEvent::default(),
281                ));
282            }
283            return;
284        }
285
286        if self
287            .touch_event_simulator
288            .as_ref()
289            .is_some_and(|touch_event_simulator| {
290                touch_event_simulator.maybe_consume_mouse_move_event(webview, point)
291            })
292        {
293            return;
294        }
295
296        webview.notify_input_event(InputEvent::MouseMove(MouseMoveEvent::new(point.into())));
297    }
298
299    /// Handle key events before sending them to Servo.
300    fn handle_intercepted_key_bindings(
301        &self,
302        state: Rc<RunningAppState>,
303        key_event: &KeyboardEvent,
304    ) -> bool {
305        let Some(focused_webview) = state.focused_webview() else {
306            return false;
307        };
308
309        let mut handled = true;
310        ShortcutMatcher::from_event(key_event.event.clone())
311            .shortcut(CMD_OR_CONTROL, 'R', || focused_webview.reload())
312            .shortcut(CMD_OR_CONTROL, 'W', || {
313                state.close_webview(focused_webview.id());
314            })
315            .shortcut(CMD_OR_CONTROL, 'P', || {
316                let rate = env::var("SAMPLING_RATE")
317                    .ok()
318                    .and_then(|s| s.parse().ok())
319                    .unwrap_or(10);
320                let duration = env::var("SAMPLING_DURATION")
321                    .ok()
322                    .and_then(|s| s.parse().ok())
323                    .unwrap_or(10);
324                focused_webview.toggle_sampling_profiler(
325                    Duration::from_millis(rate),
326                    Duration::from_secs(duration),
327                );
328            })
329            .shortcut(CMD_OR_CONTROL, 'X', || {
330                focused_webview
331                    .notify_input_event(InputEvent::EditingAction(servo::EditingActionEvent::Cut));
332            })
333            .shortcut(CMD_OR_CONTROL, 'C', || {
334                focused_webview
335                    .notify_input_event(InputEvent::EditingAction(servo::EditingActionEvent::Copy));
336            })
337            .shortcut(CMD_OR_CONTROL, 'V', || {
338                focused_webview.notify_input_event(InputEvent::EditingAction(
339                    servo::EditingActionEvent::Paste,
340                ));
341            })
342            .shortcut(Modifiers::CONTROL, Key::Named(NamedKey::F9), || {
343                focused_webview.capture_webrender();
344            })
345            .shortcut(Modifiers::CONTROL, Key::Named(NamedKey::F10), || {
346                focused_webview.toggle_webrender_debugging(WebRenderDebugOption::RenderTargetDebug);
347            })
348            .shortcut(Modifiers::CONTROL, Key::Named(NamedKey::F11), || {
349                focused_webview.toggle_webrender_debugging(WebRenderDebugOption::TextureCacheDebug);
350            })
351            .shortcut(Modifiers::CONTROL, Key::Named(NamedKey::F12), || {
352                focused_webview.toggle_webrender_debugging(WebRenderDebugOption::Profiler);
353            })
354            .shortcut(CMD_OR_ALT, Key::Named(NamedKey::ArrowRight), || {
355                focused_webview.go_forward(1);
356            })
357            .optional_shortcut(
358                cfg!(not(target_os = "windows")),
359                CMD_OR_CONTROL,
360                ']',
361                || {
362                    focused_webview.go_forward(1);
363                },
364            )
365            .shortcut(CMD_OR_ALT, Key::Named(NamedKey::ArrowLeft), || {
366                focused_webview.go_back(1);
367            })
368            .optional_shortcut(
369                cfg!(not(target_os = "windows")),
370                CMD_OR_CONTROL,
371                '[',
372                || {
373                    focused_webview.go_back(1);
374                },
375            )
376            .optional_shortcut(
377                self.get_fullscreen(),
378                Modifiers::empty(),
379                Key::Named(NamedKey::Escape),
380                || focused_webview.exit_fullscreen(),
381            )
382            // Select the first 8 tabs via shortcuts
383            .shortcut(CMD_OR_CONTROL, '1', || state.focus_webview_by_index(0))
384            .shortcut(CMD_OR_CONTROL, '2', || state.focus_webview_by_index(1))
385            .shortcut(CMD_OR_CONTROL, '3', || state.focus_webview_by_index(2))
386            .shortcut(CMD_OR_CONTROL, '4', || state.focus_webview_by_index(3))
387            .shortcut(CMD_OR_CONTROL, '5', || state.focus_webview_by_index(4))
388            .shortcut(CMD_OR_CONTROL, '6', || state.focus_webview_by_index(5))
389            .shortcut(CMD_OR_CONTROL, '7', || state.focus_webview_by_index(6))
390            .shortcut(CMD_OR_CONTROL, '8', || state.focus_webview_by_index(7))
391            // Cmd/Ctrl 9 is a bit different in that it focuses the last tab instead of the 9th
392            .shortcut(CMD_OR_CONTROL, '9', || {
393                let len = state.webviews().len();
394                if len > 0 {
395                    state.focus_webview_by_index(len - 1)
396                }
397            })
398            .shortcut(Modifiers::CONTROL, Key::Named(NamedKey::PageDown), || {
399                if let Some(index) = state.get_focused_webview_index() {
400                    state.focus_webview_by_index((index + 1) % state.webviews().len())
401                }
402            })
403            .shortcut(Modifiers::CONTROL, Key::Named(NamedKey::PageUp), || {
404                if let Some(index) = state.get_focused_webview_index() {
405                    let len = state.webviews().len();
406                    state.focus_webview_by_index((index + len - 1) % len);
407                }
408            })
409            .shortcut(CMD_OR_CONTROL, 'T', || {
410                state.create_and_focus_toplevel_webview(
411                    Url::parse("servo:newtab")
412                        .expect("Should be able to unconditionally parse 'servo:newtab' as URL"),
413                );
414            })
415            .shortcut(CMD_OR_CONTROL, 'Q', || state.servo().start_shutting_down())
416            .otherwise(|| handled = false);
417        handled
418    }
419
420    pub(crate) fn offscreen_rendering_context(&self) -> Rc<OffscreenRenderingContext> {
421        self.rendering_context.clone()
422    }
423
424    #[allow(unused_variables)]
425    fn force_srgb_color_space(window_handle: RawWindowHandle) {
426        #[cfg(target_os = "macos")]
427        {
428            if let RawWindowHandle::AppKit(handle) = window_handle {
429                assert!(MainThreadMarker::new().is_some());
430                unsafe {
431                    let view = handle.ns_view.cast::<NSView>().as_ref();
432                    view.window()
433                        .expect("Should have a window")
434                        .setColorSpace(Some(&NSColorSpace::sRGBColorSpace()));
435                }
436            }
437        }
438    }
439}
440
441impl WindowPortsMethods for Window {
442    fn screen_geometry(&self) -> ScreenGeometry {
443        let hidpi_factor = self.hidpi_scale_factor();
444        let toolbar_size = Size2D::new(
445            0.0,
446            (self.toolbar_height.get() * self.hidpi_scale_factor()).0,
447        );
448        let screen_size = self.screen_size.to_f32() * hidpi_factor;
449
450        // FIXME: In reality, this should subtract screen space used by the system interface
451        // elements, but it is difficult to get this value with `winit` currently. See:
452        // See https://github.com/rust-windowing/winit/issues/2494
453        let available_screen_size = screen_size - toolbar_size;
454
455        let window_rect = DeviceIntRect::from_origin_and_size(
456            winit_position_to_euclid_point(self.winit_window.outer_position().unwrap_or_default()),
457            winit_size_to_euclid_size(self.winit_window.outer_size()).to_i32(),
458        );
459
460        ScreenGeometry {
461            size: screen_size.to_i32(),
462            available_size: available_screen_size.to_i32(),
463            window_rect,
464        }
465    }
466
467    fn device_hidpi_scale_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel> {
468        Scale::new(self.winit_window.scale_factor() as f32)
469    }
470
471    fn hidpi_scale_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel> {
472        self.device_pixel_ratio_override
473            .map(Scale::new)
474            .unwrap_or_else(|| self.device_hidpi_scale_factor())
475    }
476
477    fn set_title(&self, title: &str) {
478        self.winit_window.set_title(title);
479    }
480
481    fn set_title_if_changed(&self, title: &str) -> bool {
482        let mut last = self.last_title.borrow_mut();
483        if *last == title {
484            return false;
485        }
486
487        self.winit_window.set_title(title);
488        *last = title.to_owned();
489        true
490    }
491
492    fn request_resize(&self, _: &WebView, new_outer_size: DeviceIntSize) -> Option<DeviceIntSize> {
493        // Allocate space for the window deocrations, but do not let the inner size get
494        // smaller than `MIN_WINDOW_INNER_SIZE` or larger than twice the screen size.
495        let inner_size = self.winit_window.inner_size();
496        let outer_size = self.winit_window.outer_size();
497        let decoration_size: DeviceIntSize = Size2D::new(
498            outer_size.height - inner_size.height,
499            outer_size.width - inner_size.width,
500        )
501        .cast();
502
503        let screen_size = (self.screen_size.to_f32() * self.hidpi_scale_factor()).to_i32();
504        let new_outer_size =
505            new_outer_size.clamp(MIN_WINDOW_INNER_SIZE + decoration_size, screen_size * 2);
506
507        if outer_size.width == new_outer_size.width as u32 &&
508            outer_size.height == new_outer_size.height as u32
509        {
510            return Some(new_outer_size);
511        }
512
513        let new_inner_size = new_outer_size - decoration_size;
514        self.winit_window
515            .request_inner_size(PhysicalSize::new(
516                new_inner_size.width,
517                new_inner_size.height,
518            ))
519            .map(|resulting_size| {
520                DeviceIntSize::new(
521                    resulting_size.width as i32 + decoration_size.width,
522                    resulting_size.height as i32 + decoration_size.height,
523                )
524            })
525    }
526
527    fn window_rect(&self) -> DeviceIndependentIntRect {
528        let outer_size = self.winit_window.outer_size();
529        let scale = self.hidpi_scale_factor();
530
531        let outer_size = winit_size_to_euclid_size(outer_size).to_i32();
532
533        let origin = self
534            .winit_window
535            .outer_position()
536            .map(winit_position_to_euclid_point)
537            .unwrap_or_default();
538        convert_rect_to_css_pixel(
539            DeviceIntRect::from_origin_and_size(origin, outer_size),
540            scale,
541        )
542    }
543
544    fn set_position(&self, point: DeviceIntPoint) {
545        self.winit_window
546            .set_outer_position::<PhysicalPosition<i32>>(PhysicalPosition::new(point.x, point.y))
547    }
548
549    fn set_fullscreen(&self, state: bool) {
550        if self.fullscreen.get() != state {
551            self.winit_window.set_fullscreen(if state {
552                Some(winit::window::Fullscreen::Borderless(Some(
553                    self.monitor.clone(),
554                )))
555            } else {
556                None
557            });
558        }
559        self.fullscreen.set(state);
560    }
561
562    fn get_fullscreen(&self) -> bool {
563        self.fullscreen.get()
564    }
565
566    fn set_cursor(&self, cursor: Cursor) {
567        use winit::window::CursorIcon;
568
569        let winit_cursor = match cursor {
570            Cursor::Default => CursorIcon::Default,
571            Cursor::Pointer => CursorIcon::Pointer,
572            Cursor::ContextMenu => CursorIcon::ContextMenu,
573            Cursor::Help => CursorIcon::Help,
574            Cursor::Progress => CursorIcon::Progress,
575            Cursor::Wait => CursorIcon::Wait,
576            Cursor::Cell => CursorIcon::Cell,
577            Cursor::Crosshair => CursorIcon::Crosshair,
578            Cursor::Text => CursorIcon::Text,
579            Cursor::VerticalText => CursorIcon::VerticalText,
580            Cursor::Alias => CursorIcon::Alias,
581            Cursor::Copy => CursorIcon::Copy,
582            Cursor::Move => CursorIcon::Move,
583            Cursor::NoDrop => CursorIcon::NoDrop,
584            Cursor::NotAllowed => CursorIcon::NotAllowed,
585            Cursor::Grab => CursorIcon::Grab,
586            Cursor::Grabbing => CursorIcon::Grabbing,
587            Cursor::EResize => CursorIcon::EResize,
588            Cursor::NResize => CursorIcon::NResize,
589            Cursor::NeResize => CursorIcon::NeResize,
590            Cursor::NwResize => CursorIcon::NwResize,
591            Cursor::SResize => CursorIcon::SResize,
592            Cursor::SeResize => CursorIcon::SeResize,
593            Cursor::SwResize => CursorIcon::SwResize,
594            Cursor::WResize => CursorIcon::WResize,
595            Cursor::EwResize => CursorIcon::EwResize,
596            Cursor::NsResize => CursorIcon::NsResize,
597            Cursor::NeswResize => CursorIcon::NeswResize,
598            Cursor::NwseResize => CursorIcon::NwseResize,
599            Cursor::ColResize => CursorIcon::ColResize,
600            Cursor::RowResize => CursorIcon::RowResize,
601            Cursor::AllScroll => CursorIcon::AllScroll,
602            Cursor::ZoomIn => CursorIcon::ZoomIn,
603            Cursor::ZoomOut => CursorIcon::ZoomOut,
604            Cursor::None => {
605                self.winit_window.set_cursor_visible(false);
606                return;
607            },
608        };
609        self.winit_window.set_cursor(winit_cursor);
610        self.winit_window.set_cursor_visible(true);
611    }
612
613    fn id(&self) -> winit::window::WindowId {
614        self.winit_window.id()
615    }
616
617    fn handle_winit_event(&self, state: Rc<RunningAppState>, event: WindowEvent) {
618        // Make sure to handle early resize events even when there are no webviews yet
619        if let WindowEvent::Resized(new_inner_size) = event {
620            if self.inner_size.get() != new_inner_size {
621                self.inner_size.set(new_inner_size);
622                // This should always be set to inner size
623                // because we are resizing `SurfmanRenderingContext`.
624                // See https://github.com/servo/servo/issues/38369#issuecomment-3138378527
625                self.window_rendering_context.resize(new_inner_size);
626            }
627            return;
628        }
629
630        let Some(webview) = state.focused_webview() else {
631            return;
632        };
633
634        match event {
635            WindowEvent::KeyboardInput { event, .. } => self.handle_keyboard_input(state, event),
636            WindowEvent::ModifiersChanged(modifiers) => self.modifiers_state.set(modifiers.state()),
637            WindowEvent::MouseInput { state, button, .. } => {
638                self.handle_mouse_button_event(&webview, button, state);
639            },
640            WindowEvent::CursorMoved { position, .. } => {
641                self.handle_mouse_move_event(&webview, position);
642            },
643            WindowEvent::CursorLeft { .. } => {
644                if webview
645                    .rect()
646                    .contains(self.webview_relative_mouse_point.get())
647                {
648                    webview.notify_input_event(InputEvent::MouseLeftViewport(
649                        MouseLeftViewportEvent::default(),
650                    ));
651                }
652            },
653            WindowEvent::MouseWheel { delta, .. } => {
654                let (delta_x, delta_y, mode) = match delta {
655                    MouseScrollDelta::LineDelta(delta_x, delta_y) => (
656                        (delta_x * LINE_WIDTH) as f64,
657                        (delta_y * LINE_HEIGHT) as f64,
658                        WheelMode::DeltaLine,
659                    ),
660                    MouseScrollDelta::PixelDelta(delta) => {
661                        (delta.x, delta.y, WheelMode::DeltaPixel)
662                    },
663                };
664
665                // Create wheel event before snapping to the major axis of movement
666                let delta = WheelDelta {
667                    x: delta_x,
668                    y: delta_y,
669                    z: 0.0,
670                    mode,
671                };
672                let point = self.webview_relative_mouse_point.get();
673                webview.notify_input_event(InputEvent::Wheel(WheelEvent::new(delta, point.into())));
674            },
675            WindowEvent::Touch(touch) => {
676                webview.notify_input_event(InputEvent::Touch(TouchEvent::new(
677                    winit_phase_to_touch_event_type(touch.phase),
678                    TouchId(touch.id as i32),
679                    DevicePoint::new(touch.location.x as f32, touch.location.y as f32).into(),
680                )));
681            },
682            WindowEvent::PinchGesture { delta, .. } => {
683                webview.pinch_zoom(delta as f32 + 1.0, self.webview_relative_mouse_point.get());
684            },
685            WindowEvent::CloseRequested => {
686                state.servo().start_shutting_down();
687            },
688            WindowEvent::ThemeChanged(theme) => {
689                webview.notify_theme_change(match theme {
690                    winit::window::Theme::Light => Theme::Light,
691                    winit::window::Theme::Dark => Theme::Dark,
692                });
693            },
694            WindowEvent::Ime(ime) => match ime {
695                Ime::Enabled => {
696                    webview.notify_input_event(InputEvent::Ime(ImeEvent::Composition(
697                        servo::CompositionEvent {
698                            state: servo::CompositionState::Start,
699                            data: String::new(),
700                        },
701                    )));
702                },
703                Ime::Preedit(text, _) => {
704                    webview.notify_input_event(InputEvent::Ime(ImeEvent::Composition(
705                        servo::CompositionEvent {
706                            state: servo::CompositionState::Update,
707                            data: text,
708                        },
709                    )));
710                },
711                Ime::Commit(text) => {
712                    webview.notify_input_event(InputEvent::Ime(ImeEvent::Composition(
713                        servo::CompositionEvent {
714                            state: servo::CompositionState::End,
715                            data: text,
716                        },
717                    )));
718                },
719                Ime::Disabled => {
720                    webview.notify_input_event(InputEvent::Ime(ImeEvent::Dismissed));
721                },
722            },
723            _ => {},
724        }
725    }
726
727    #[cfg(feature = "webxr")]
728    fn new_glwindow(
729        &self,
730        event_loop: &ActiveEventLoop,
731    ) -> Rc<dyn servo::webxr::glwindow::GlWindow> {
732        let size = self.winit_window.outer_size();
733
734        let window_attr = winit::window::Window::default_attributes()
735            .with_title("Servo XR".to_string())
736            .with_inner_size(size)
737            .with_visible(false);
738
739        let winit_window = event_loop
740            .create_window(window_attr)
741            .expect("Failed to create window.");
742
743        let pose = Rc::new(XRWindowPose {
744            xr_rotation: Cell::new(Rotation3D::identity()),
745            xr_translation: Cell::new(Vector3D::zero()),
746        });
747        self.xr_window_poses.borrow_mut().push(pose.clone());
748        Rc::new(XRWindow { winit_window, pose })
749    }
750
751    fn winit_window(&self) -> Option<&winit::window::Window> {
752        Some(&self.winit_window)
753    }
754
755    fn toolbar_height(&self) -> Length<f32, DeviceIndependentPixel> {
756        self.toolbar_height.get()
757    }
758
759    fn set_toolbar_height(&self, height: Length<f32, DeviceIndependentPixel>) {
760        if self.toolbar_height() == height {
761            return;
762        }
763        self.toolbar_height.set(height);
764    }
765
766    fn rendering_context(&self) -> Rc<dyn RenderingContext> {
767        self.rendering_context.clone()
768    }
769
770    fn show_ime(&self, input_method: InputMethodControl) {
771        let position = input_method.position();
772        self.winit_window.set_ime_allowed(true);
773        self.winit_window.set_ime_cursor_area(
774            LogicalPosition::new(
775                position.min.x,
776                position.min.y + (self.toolbar_height.get().0 as i32),
777            ),
778            LogicalSize::new(
779                position.max.x - position.min.x,
780                position.max.y - position.min.y,
781            ),
782        );
783    }
784
785    fn hide_ime(&self) {
786        self.winit_window.set_ime_allowed(false);
787    }
788
789    fn theme(&self) -> servo::Theme {
790        match self.winit_window.theme() {
791            Some(winit::window::Theme::Dark) => servo::Theme::Dark,
792            Some(winit::window::Theme::Light) | None => servo::Theme::Light,
793        }
794    }
795
796    fn maximize(&self, _webview: &WebView) {
797        self.winit_window.set_maximized(true);
798    }
799
800    /// Handle servoshell key bindings that may have been prevented by the page in the focused webview.
801    fn notify_input_event_handled(
802        &self,
803        webview: &WebView,
804        id: InputEventId,
805        result: InputEventResult,
806    ) {
807        let Some(keyboard_event) = self.pending_keyboard_events.borrow_mut().remove(&id) else {
808            return;
809        };
810        if result.intersects(InputEventResult::DefaultPrevented | InputEventResult::Consumed) {
811            return;
812        }
813
814        ShortcutMatcher::from_event(keyboard_event.event)
815            .shortcut(CMD_OR_CONTROL, '=', || {
816                webview.set_page_zoom(webview.page_zoom() + 0.1);
817            })
818            .shortcut(CMD_OR_CONTROL, '+', || {
819                webview.set_page_zoom(webview.page_zoom() + 0.1);
820            })
821            .shortcut(CMD_OR_CONTROL, '-', || {
822                webview.set_page_zoom(webview.page_zoom() - 0.1);
823            })
824            .shortcut(CMD_OR_CONTROL, '0', || {
825                webview.set_page_zoom(1.0);
826            });
827    }
828}
829
830fn winit_phase_to_touch_event_type(phase: TouchPhase) -> TouchEventType {
831    match phase {
832        TouchPhase::Started => TouchEventType::Down,
833        TouchPhase::Moved => TouchEventType::Move,
834        TouchPhase::Ended => TouchEventType::Up,
835        TouchPhase::Cancelled => TouchEventType::Cancel,
836    }
837}
838
839#[cfg(any(target_os = "linux", target_os = "windows"))]
840fn load_icon(icon_bytes: &[u8]) -> Icon {
841    let (icon_rgba, icon_width, icon_height) = {
842        use image::{GenericImageView, Pixel};
843        let image = image::load_from_memory(icon_bytes).expect("Failed to load icon");
844        let (width, height) = image.dimensions();
845        let mut rgba = Vec::with_capacity((width * height) as usize * 4);
846        for (_, _, pixel) in image.pixels() {
847            rgba.extend_from_slice(&pixel.to_rgba().0);
848        }
849        (rgba, width, height)
850    };
851    Icon::from_rgba(icon_rgba, icon_width, icon_height).expect("Failed to load icon")
852}
853
854#[cfg(feature = "webxr")]
855struct XRWindow {
856    winit_window: winit::window::Window,
857    pose: Rc<XRWindowPose>,
858}
859
860struct XRWindowPose {
861    xr_rotation: Cell<Rotation3D<f32, UnknownUnit, UnknownUnit>>,
862    xr_translation: Cell<Vector3D<f32, UnknownUnit>>,
863}
864
865#[cfg(feature = "webxr")]
866impl servo::webxr::glwindow::GlWindow for XRWindow {
867    fn get_render_target(
868        &self,
869        device: &mut surfman::Device,
870        _context: &mut surfman::Context,
871    ) -> servo::webxr::glwindow::GlWindowRenderTarget {
872        self.winit_window.set_visible(true);
873        let window_handle = self
874            .winit_window
875            .window_handle()
876            .expect("could not get window handle from window");
877        let size = self.winit_window.inner_size();
878        let size = Size2D::new(size.width as i32, size.height as i32);
879        let native_widget = device
880            .connection()
881            .create_native_widget_from_window_handle(window_handle, size)
882            .expect("Failed to create native widget");
883        servo::webxr::glwindow::GlWindowRenderTarget::NativeWidget(native_widget)
884    }
885
886    fn get_rotation(&self) -> Rotation3D<f32, UnknownUnit, UnknownUnit> {
887        self.pose.xr_rotation.get()
888    }
889
890    fn get_translation(&self) -> Vector3D<f32, UnknownUnit> {
891        self.pose.xr_translation.get()
892    }
893
894    fn get_mode(&self) -> servo::webxr::glwindow::GlWindowMode {
895        use servo::servo_config::pref;
896        if pref!(dom_webxr_glwindow_red_cyan) {
897            servo::webxr::glwindow::GlWindowMode::StereoRedCyan
898        } else if pref!(dom_webxr_glwindow_left_right) {
899            servo::webxr::glwindow::GlWindowMode::StereoLeftRight
900        } else if pref!(dom_webxr_glwindow_spherical) {
901            servo::webxr::glwindow::GlWindowMode::Spherical
902        } else if pref!(dom_webxr_glwindow_cubemap) {
903            servo::webxr::glwindow::GlWindowMode::Cubemap
904        } else {
905            servo::webxr::glwindow::GlWindowMode::Blit
906        }
907    }
908
909    fn display_handle(&self) -> raw_window_handle::DisplayHandle<'_> {
910        self.winit_window
911            .display_handle()
912            .expect("Every window should have a display handle")
913    }
914}
915
916impl XRWindowPose {
917    fn handle_xr_translation(&self, input: &KeyboardEvent) {
918        if input.event.state != KeyState::Down {
919            return;
920        }
921        const NORMAL_TRANSLATE: f32 = 0.1;
922        const QUICK_TRANSLATE: f32 = 1.0;
923        let mut x = 0.0;
924        let mut z = 0.0;
925        match input.event.key {
926            Key::Character(ref k) => match &**k {
927                "w" => z = -NORMAL_TRANSLATE,
928                "W" => z = -QUICK_TRANSLATE,
929                "s" => z = NORMAL_TRANSLATE,
930                "S" => z = QUICK_TRANSLATE,
931                "a" => x = -NORMAL_TRANSLATE,
932                "A" => x = -QUICK_TRANSLATE,
933                "d" => x = NORMAL_TRANSLATE,
934                "D" => x = QUICK_TRANSLATE,
935                _ => return,
936            },
937            _ => return,
938        };
939        let (old_x, old_y, old_z) = self.xr_translation.get().to_tuple();
940        let vec = Vector3D::new(x + old_x, old_y, z + old_z);
941        self.xr_translation.set(vec);
942    }
943
944    fn handle_xr_rotation(&self, input: &KeyEvent, modifiers: ModifiersState) {
945        if input.state != ElementState::Pressed {
946            return;
947        }
948        let mut x = 0.0;
949        let mut y = 0.0;
950        match input.logical_key {
951            LogicalKey::Named(WinitNamedKey::ArrowUp) => x = 1.0,
952            LogicalKey::Named(WinitNamedKey::ArrowDown) => x = -1.0,
953            LogicalKey::Named(WinitNamedKey::ArrowLeft) => y = 1.0,
954            LogicalKey::Named(WinitNamedKey::ArrowRight) => y = -1.0,
955            _ => return,
956        };
957        if modifiers.shift_key() {
958            x *= 10.0;
959            y *= 10.0;
960        }
961        let x: Rotation3D<_, UnknownUnit, UnknownUnit> = Rotation3D::around_x(Angle::degrees(x));
962        let y: Rotation3D<_, UnknownUnit, UnknownUnit> = Rotation3D::around_y(Angle::degrees(y));
963        let rotation = self.xr_rotation.get().then(&x).then(&y);
964        self.xr_rotation.set(rotation);
965    }
966}
967
968#[derive(Default)]
969pub struct TouchEventSimulator {
970    pub left_mouse_button_down: Cell<bool>,
971}
972
973impl TouchEventSimulator {
974    fn maybe_consume_move_button_event(
975        &self,
976        webview: &WebView,
977        button: MouseButton,
978        action: ElementState,
979        point: DevicePoint,
980    ) -> bool {
981        if button != MouseButton::Left {
982            return false;
983        }
984
985        if action == ElementState::Pressed && !self.left_mouse_button_down.get() {
986            webview.notify_input_event(InputEvent::Touch(TouchEvent::new(
987                TouchEventType::Down,
988                TouchId(0),
989                point.into(),
990            )));
991            self.left_mouse_button_down.set(true);
992        } else if action == ElementState::Released {
993            webview.notify_input_event(InputEvent::Touch(TouchEvent::new(
994                TouchEventType::Up,
995                TouchId(0),
996                point.into(),
997            )));
998            self.left_mouse_button_down.set(false);
999        }
1000
1001        true
1002    }
1003
1004    fn maybe_consume_mouse_move_event(
1005        &self,
1006        webview: &WebView,
1007        point: Point2D<f32, DevicePixel>,
1008    ) -> bool {
1009        if !self.left_mouse_button_down.get() {
1010            return false;
1011        }
1012
1013        webview.notify_input_event(InputEvent::Touch(TouchEvent::new(
1014            TouchEventType::Move,
1015            TouchId(0),
1016            point.into(),
1017        )));
1018        true
1019    }
1020}