1#![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, info};
19use raw_window_handle::{HasDisplayHandle, HasWindowHandle, RawWindowHandle};
20use servo::{
21 AuthenticationRequest, Cursor, DeviceIndependentIntRect, DeviceIndependentPixel,
22 DeviceIntPoint, DeviceIntRect, DeviceIntSize, DevicePixel, DevicePoint, EmbedderControl,
23 EmbedderControlId, GenericSender, 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, WebRenderDebugOption, WebView, WebViewId, WheelDelta,
28 WheelEvent, WheelMode, WindowRenderingContext, convert_rect_to_css_pixel,
29};
30use url::Url;
31use winit::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize};
32use winit::event::{
33 ElementState, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase, WindowEvent,
34};
35use winit::event_loop::{ActiveEventLoop, EventLoopProxy};
36use winit::keyboard::{Key as LogicalKey, ModifiersState, NamedKey as WinitNamedKey};
37#[cfg(target_os = "linux")]
38use winit::platform::wayland::WindowAttributesExtWayland;
39#[cfg(any(target_os = "linux", target_os = "windows"))]
40use winit::window::Icon;
41#[cfg(target_os = "macos")]
42use {
43 objc2_app_kit::{NSColorSpace, NSView},
44 objc2_foundation::MainThreadMarker,
45};
46
47use super::geometry::{winit_position_to_euclid_point, winit_size_to_euclid_size};
48use super::keyutils::{CMD_OR_ALT, keyboard_event_from_winit};
49use crate::desktop::accelerated_gl_media::setup_gl_accelerated_media;
50use crate::desktop::dialog::Dialog;
51use crate::desktop::event_loop::AppEvent;
52use crate::desktop::gui::Gui;
53use crate::desktop::keyutils::CMD_OR_CONTROL;
54use crate::prefs::ServoShellPreferences;
55use crate::running_app_state::{RunningAppState, UserInterfaceCommand};
56use crate::window::{
57 LINE_HEIGHT, LINE_WIDTH, MIN_WINDOW_INNER_SIZE, PlatformWindow, ServoShellWindow,
58 ServoShellWindowId,
59};
60
61pub(crate) const INITIAL_WINDOW_TITLE: &str = "Servo";
62
63pub struct Window {
64 gui: RefCell<Gui>,
67 screen_size: Size2D<u32, DeviceIndependentPixel>,
68 monitor: winit::monitor::MonitorHandle,
69 webview_relative_mouse_point: Cell<Point2D<f32, DevicePixel>>,
70 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 rendering_context: Rc<OffscreenRenderingContext>,
80 window_rendering_context: Rc<WindowRenderingContext>,
84 touch_event_simulator: Option<TouchEventSimulator>,
87 pending_keyboard_events: RefCell<HashMap<InputEventId, KeyboardEvent>>,
91 winit_window: winit::window::Window,
95 last_title: RefCell<String>,
98 dialogs: RefCell<HashMap<WebViewId, Vec<Dialog>>>,
100 visible_input_methods: RefCell<Vec<EmbedderControlId>>,
102}
103
104impl Window {
105 pub(crate) fn new(
106 servoshell_preferences: &ServoShellPreferences,
107 event_loop: &ActiveEventLoop,
108 event_loop_proxy: EventLoopProxy<AppEvent>,
109 initial_url: Url,
110 ) -> Rc<Self> {
111 let no_native_titlebar = servoshell_preferences.no_native_titlebar;
112 let inner_size = servoshell_preferences.initial_window_size;
113 let window_attr = winit::window::Window::default_attributes()
114 .with_title(INITIAL_WINDOW_TITLE.to_string())
115 .with_decorations(!no_native_titlebar)
116 .with_transparent(no_native_titlebar)
117 .with_inner_size(LogicalSize::new(inner_size.width, inner_size.height))
118 .with_min_inner_size(LogicalSize::new(
119 MIN_WINDOW_INNER_SIZE.width,
120 MIN_WINDOW_INNER_SIZE.height,
121 ))
122 .with_visible(false);
125
126 #[cfg(target_os = "linux")]
128 let window_attr = window_attr.with_name("org.servo.Servo", "Servo");
129
130 #[allow(deprecated)]
131 let winit_window = event_loop
132 .create_window(window_attr)
133 .expect("Failed to create window.");
134
135 #[cfg(any(target_os = "linux", target_os = "windows"))]
136 {
137 let icon_bytes = include_bytes!("../../../resources/servo_64.png");
138 winit_window.set_window_icon(Some(load_icon(icon_bytes)));
139 }
140
141 let window_handle = winit_window
142 .window_handle()
143 .expect("winit window did not have a window handle");
144 Window::force_srgb_color_space(window_handle.as_raw());
145
146 let monitor = winit_window
147 .current_monitor()
148 .or_else(|| winit_window.available_monitors().nth(0))
149 .expect("No monitor detected");
150
151 let (screen_size, screen_scale) = servoshell_preferences.screen_size_override.map_or_else(
152 || (monitor.size(), winit_window.scale_factor()),
153 |size| (PhysicalSize::new(size.width, size.height), 1.0),
154 );
155 let screen_scale: Scale<f64, DeviceIndependentPixel, DevicePixel> =
156 Scale::new(screen_scale);
157 let screen_size = (winit_size_to_euclid_size(screen_size).to_f64() / screen_scale).to_u32();
158 let inner_size = winit_window.inner_size();
159
160 let display_handle = event_loop
161 .display_handle()
162 .expect("could not get display handle from window");
163 let window_handle = winit_window
164 .window_handle()
165 .expect("could not get window handle from window");
166 let window_rendering_context = Rc::new(
167 WindowRenderingContext::new(display_handle, window_handle, inner_size)
168 .expect("Could not create RenderingContext for Window"),
169 );
170
171 {
174 let details = window_rendering_context.surfman_details();
175 setup_gl_accelerated_media(details.0, details.1);
176 }
177
178 window_rendering_context
180 .make_current()
181 .expect("Could not make window RenderingContext current");
182
183 let rendering_context = Rc::new(window_rendering_context.offscreen_context(inner_size));
184 let gui = RefCell::new(Gui::new(
185 &winit_window,
186 event_loop,
187 event_loop_proxy,
188 rendering_context.clone(),
189 initial_url,
190 ));
191
192 debug!("Created window {:?}", winit_window.id());
193 Rc::new(Window {
194 gui,
195 winit_window,
196 webview_relative_mouse_point: Cell::new(Point2D::zero()),
197 fullscreen: Cell::new(false),
198 inner_size: Cell::new(inner_size),
199 monitor,
200 screen_size,
201 device_pixel_ratio_override: servoshell_preferences.device_pixel_ratio_override,
202 xr_window_poses: RefCell::new(vec![]),
203 modifiers_state: Cell::new(ModifiersState::empty()),
204 window_rendering_context,
205 touch_event_simulator: servoshell_preferences
206 .simulate_touch_events
207 .then(Default::default),
208 pending_keyboard_events: Default::default(),
209 rendering_context,
210 last_title: RefCell::new(String::from(INITIAL_WINDOW_TITLE)),
211 dialogs: Default::default(),
212 visible_input_methods: Default::default(),
213 })
214 }
215
216 pub(crate) fn winit_window(&self) -> &winit::window::Window {
217 &self.winit_window
218 }
219
220 fn handle_keyboard_input(
221 &self,
222 state: Rc<RunningAppState>,
223 window: &ServoShellWindow,
224 winit_event: KeyEvent,
225 ) {
226 let keyboard_event = keyboard_event_from_winit(&winit_event, self.modifiers_state.get());
228 if self.handle_intercepted_key_bindings(state.clone(), window, &keyboard_event) {
229 return;
230 }
231
232 let Some(webview) = window.active_webview() else {
234 return;
235 };
236
237 for xr_window_pose in self.xr_window_poses.borrow().iter() {
238 xr_window_pose.handle_xr_rotation(&winit_event, self.modifiers_state.get());
239 xr_window_pose.handle_xr_translation(&keyboard_event);
240 }
241
242 let id = webview.notify_input_event(InputEvent::Keyboard(keyboard_event.clone()));
243 self.pending_keyboard_events
244 .borrow_mut()
245 .insert(id, keyboard_event);
246 }
247
248 fn handle_mouse_button_event(
250 &self,
251 webview: &WebView,
252 button: MouseButton,
253 action: ElementState,
254 ) {
255 let point = self.webview_relative_mouse_point.get();
257 if !webview.rect().contains(point) {
258 return;
259 }
260
261 if self
262 .touch_event_simulator
263 .as_ref()
264 .is_some_and(|touch_event_simulator| {
265 touch_event_simulator
266 .maybe_consume_move_button_event(webview, button, action, point)
267 })
268 {
269 return;
270 }
271
272 let mouse_button = match &button {
273 MouseButton::Left => ServoMouseButton::Left,
274 MouseButton::Right => ServoMouseButton::Right,
275 MouseButton::Middle => ServoMouseButton::Middle,
276 MouseButton::Back => ServoMouseButton::Back,
277 MouseButton::Forward => ServoMouseButton::Forward,
278 MouseButton::Other(value) => ServoMouseButton::Other(*value),
279 };
280
281 let action = match action {
282 ElementState::Pressed => MouseButtonAction::Down,
283 ElementState::Released => MouseButtonAction::Up,
284 };
285
286 webview.notify_input_event(InputEvent::MouseButton(MouseButtonEvent::new(
287 action,
288 mouse_button,
289 point.into(),
290 )));
291 }
292
293 fn handle_mouse_move_event(&self, webview: &WebView, position: PhysicalPosition<f64>) {
295 let mut point = winit_position_to_euclid_point(position).to_f32();
296 point.y -= (self.toolbar_height() * self.hidpi_scale_factor()).0;
297
298 let previous_point = self.webview_relative_mouse_point.get();
299 self.webview_relative_mouse_point.set(point);
300
301 if !webview.rect().contains(point) {
302 if webview.rect().contains(previous_point) {
303 webview.notify_input_event(InputEvent::MouseLeftViewport(
304 MouseLeftViewportEvent::default(),
305 ));
306 }
307 return;
308 }
309
310 if self
311 .touch_event_simulator
312 .as_ref()
313 .is_some_and(|touch_event_simulator| {
314 touch_event_simulator.maybe_consume_mouse_move_event(webview, point)
315 })
316 {
317 return;
318 }
319
320 webview.notify_input_event(InputEvent::MouseMove(MouseMoveEvent::new(point.into())));
321 }
322
323 fn handle_intercepted_key_bindings(
325 &self,
326 state: Rc<RunningAppState>,
327 window: &ServoShellWindow,
328 key_event: &KeyboardEvent,
329 ) -> bool {
330 let Some(active_webview) = window.active_webview() else {
331 return false;
332 };
333
334 let mut handled = true;
335 ShortcutMatcher::from_event(key_event.event.clone())
336 .shortcut(CMD_OR_CONTROL, 'R', || active_webview.reload())
337 .shortcut(CMD_OR_CONTROL, 'W', || {
338 window.close_webview(active_webview.id());
339 })
340 .shortcut(CMD_OR_CONTROL, 'P', || {
341 let rate = env::var("SAMPLING_RATE")
342 .ok()
343 .and_then(|s| s.parse().ok())
344 .unwrap_or(10);
345 let duration = env::var("SAMPLING_DURATION")
346 .ok()
347 .and_then(|s| s.parse().ok())
348 .unwrap_or(10);
349 active_webview.toggle_sampling_profiler(
350 Duration::from_millis(rate),
351 Duration::from_secs(duration),
352 );
353 })
354 .shortcut(CMD_OR_CONTROL, 'X', || {
355 active_webview
356 .notify_input_event(InputEvent::EditingAction(servo::EditingActionEvent::Cut));
357 })
358 .shortcut(CMD_OR_CONTROL, 'C', || {
359 active_webview
360 .notify_input_event(InputEvent::EditingAction(servo::EditingActionEvent::Copy));
361 })
362 .shortcut(CMD_OR_CONTROL, 'V', || {
363 active_webview.notify_input_event(InputEvent::EditingAction(
364 servo::EditingActionEvent::Paste,
365 ));
366 })
367 .shortcut(Modifiers::CONTROL, Key::Named(NamedKey::F9), || {
368 active_webview.capture_webrender();
369 })
370 .shortcut(Modifiers::CONTROL, Key::Named(NamedKey::F10), || {
371 active_webview.toggle_webrender_debugging(WebRenderDebugOption::RenderTargetDebug);
372 })
373 .shortcut(Modifiers::CONTROL, Key::Named(NamedKey::F11), || {
374 active_webview.toggle_webrender_debugging(WebRenderDebugOption::TextureCacheDebug);
375 })
376 .shortcut(Modifiers::CONTROL, Key::Named(NamedKey::F12), || {
377 active_webview.toggle_webrender_debugging(WebRenderDebugOption::Profiler);
378 })
379 .shortcut(CMD_OR_ALT, Key::Named(NamedKey::ArrowRight), || {
380 active_webview.go_forward(1);
381 })
382 .optional_shortcut(
383 cfg!(not(target_os = "windows")),
384 CMD_OR_CONTROL,
385 ']',
386 || {
387 active_webview.go_forward(1);
388 },
389 )
390 .shortcut(CMD_OR_ALT, Key::Named(NamedKey::ArrowLeft), || {
391 active_webview.go_back(1);
392 })
393 .optional_shortcut(
394 cfg!(not(target_os = "windows")),
395 CMD_OR_CONTROL,
396 '[',
397 || {
398 active_webview.go_back(1);
399 },
400 )
401 .optional_shortcut(
402 self.get_fullscreen(),
403 Modifiers::empty(),
404 Key::Named(NamedKey::Escape),
405 || active_webview.exit_fullscreen(),
406 )
407 .shortcut(CMD_OR_CONTROL, '1', || window.activate_webview_by_index(0))
409 .shortcut(CMD_OR_CONTROL, '2', || window.activate_webview_by_index(1))
410 .shortcut(CMD_OR_CONTROL, '3', || window.activate_webview_by_index(2))
411 .shortcut(CMD_OR_CONTROL, '4', || window.activate_webview_by_index(3))
412 .shortcut(CMD_OR_CONTROL, '5', || window.activate_webview_by_index(4))
413 .shortcut(CMD_OR_CONTROL, '6', || window.activate_webview_by_index(5))
414 .shortcut(CMD_OR_CONTROL, '7', || window.activate_webview_by_index(6))
415 .shortcut(CMD_OR_CONTROL, '8', || window.activate_webview_by_index(7))
416 .shortcut(CMD_OR_CONTROL, '9', || {
418 let len = window.webviews().len();
419 if len > 0 {
420 window.activate_webview_by_index(len - 1)
421 }
422 })
423 .shortcut(Modifiers::CONTROL, Key::Named(NamedKey::PageDown), || {
424 if let Some(index) = window.get_active_webview_index() {
425 window.activate_webview_by_index((index + 1) % window.webviews().len())
426 }
427 })
428 .shortcut(Modifiers::CONTROL, Key::Named(NamedKey::PageUp), || {
429 if let Some(index) = window.get_active_webview_index() {
430 let len = window.webviews().len();
431 window.activate_webview_by_index((index + len - 1) % len);
432 }
433 })
434 .shortcut(CMD_OR_CONTROL, 'T', || {
435 window.create_and_activate_toplevel_webview(
436 state.clone(),
437 Url::parse("servo:newtab")
438 .expect("Should be able to unconditionally parse 'servo:newtab' as URL"),
439 );
440 })
441 .shortcut(CMD_OR_CONTROL, 'Q', || state.schedule_exit())
442 .otherwise(|| handled = false);
443 handled
444 }
445
446 #[allow(unused_variables)]
447 fn force_srgb_color_space(window_handle: RawWindowHandle) {
448 #[cfg(target_os = "macos")]
449 {
450 if let RawWindowHandle::AppKit(handle) = window_handle {
451 assert!(MainThreadMarker::new().is_some());
452 unsafe {
453 let view = handle.ns_view.cast::<NSView>().as_ref();
454 view.window()
455 .expect("Should have a window")
456 .setColorSpace(Some(&NSColorSpace::sRGBColorSpace()));
457 }
458 }
459 }
460 }
461
462 fn show_ime(&self, input_method: InputMethodControl) {
463 let position = input_method.position();
464 self.winit_window.set_ime_allowed(true);
465 self.winit_window.set_ime_cursor_area(
466 LogicalPosition::new(
467 position.min.x,
468 position.min.y + (self.toolbar_height().0 as i32),
469 ),
470 LogicalSize::new(
471 position.max.x - position.min.x,
472 position.max.y - position.min.y,
473 ),
474 );
475 }
476
477 pub(crate) fn for_each_active_dialog(
478 &self,
479 window: &ServoShellWindow,
480 callback: impl Fn(&mut Dialog) -> bool,
481 ) {
482 let Some(active_webview) = window.active_webview() else {
483 return;
484 };
485 let mut dialogs = self.dialogs.borrow_mut();
486 let Some(dialogs) = dialogs.get_mut(&active_webview.id()) else {
487 return;
488 };
489 if dialogs.is_empty() {
490 return;
491 }
492
493 self.set_cursor(Cursor::Default);
497
498 let length = dialogs.len();
499 dialogs.retain_mut(callback);
500 if length != dialogs.len() {
501 window.set_needs_repaint();
502 }
503 }
504
505 fn add_dialog(&self, webview_id: WebViewId, dialog: Dialog) {
506 self.dialogs
507 .borrow_mut()
508 .entry(webview_id)
509 .or_default()
510 .push(dialog)
511 }
512
513 fn remove_dialog(&self, webview_id: WebViewId, embedder_control_id: EmbedderControlId) {
514 let mut dialogs = self.dialogs.borrow_mut();
515 if let Some(dialogs) = dialogs.get_mut(&webview_id) {
516 dialogs.retain(|dialog| dialog.embedder_control_id() != Some(embedder_control_id));
517 }
518 dialogs.retain(|_, dialogs| !dialogs.is_empty());
519 }
520
521 fn has_active_dialog_for_webview(&self, webview_id: WebViewId) -> bool {
522 let mut dialogs = self.dialogs.borrow_mut();
524 dialogs.retain(|_, dialogs| !dialogs.is_empty());
525 dialogs.contains_key(&webview_id)
526 }
527
528 fn toolbar_height(&self) -> Length<f32, DeviceIndependentPixel> {
529 self.gui.borrow().toolbar_height()
530 }
531}
532
533impl PlatformWindow for Window {
534 fn screen_geometry(&self) -> ScreenGeometry {
535 let hidpi_factor = self.hidpi_scale_factor();
536 let toolbar_size = Size2D::new(0.0, (self.toolbar_height() * self.hidpi_scale_factor()).0);
537 let screen_size = self.screen_size.to_f32() * hidpi_factor;
538
539 let available_screen_size = screen_size - toolbar_size;
543
544 let window_rect = DeviceIntRect::from_origin_and_size(
545 winit_position_to_euclid_point(self.winit_window.outer_position().unwrap_or_default()),
546 winit_size_to_euclid_size(self.winit_window.outer_size()).to_i32(),
547 );
548
549 ScreenGeometry {
550 size: screen_size.to_i32(),
551 available_size: available_screen_size.to_i32(),
552 window_rect,
553 }
554 }
555
556 fn device_hidpi_scale_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel> {
557 Scale::new(self.winit_window.scale_factor() as f32)
558 }
559
560 fn hidpi_scale_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel> {
561 self.device_pixel_ratio_override
562 .map(Scale::new)
563 .unwrap_or_else(|| self.device_hidpi_scale_factor())
564 }
565
566 fn rebuild_user_interface(&self, state: &RunningAppState, window: &ServoShellWindow) {
567 self.gui.borrow_mut().update(state, window, self);
568 }
569
570 fn update_user_interface_state(&self, _: &RunningAppState, window: &ServoShellWindow) -> bool {
571 let title = window
572 .active_webview()
573 .and_then(|webview| {
574 webview
575 .page_title()
576 .filter(|title| !title.is_empty())
577 .map(|title| title.to_string())
578 .or_else(|| webview.url().map(|url| url.to_string()))
579 })
580 .unwrap_or_else(|| INITIAL_WINDOW_TITLE.to_string());
581 if title != *self.last_title.borrow() {
582 self.winit_window.set_title(&title);
583 *self.last_title.borrow_mut() = title;
584 }
585
586 self.gui.borrow_mut().update_webview_data(window)
587 }
588
589 fn handle_winit_window_event(
590 &self,
591 state: Rc<RunningAppState>,
592 window: &ServoShellWindow,
593 event: WindowEvent,
594 ) {
595 if event == WindowEvent::RedrawRequested {
596 let mut gui = self.gui.borrow_mut();
599 gui.update(&state, window, self);
600 gui.paint(&self.winit_window);
601 }
602
603 let mut consumed = false;
605 match event {
606 WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
607 let desired_scale_factor = self.hidpi_scale_factor().get();
611 let effective_egui_zoom_factor = desired_scale_factor / scale_factor as f32;
612
613 info!(
614 "window scale factor changed to {}, setting egui zoom factor to {}",
615 scale_factor, effective_egui_zoom_factor
616 );
617
618 self.gui
619 .borrow()
620 .set_zoom_factor(effective_egui_zoom_factor);
621
622 window.hidpi_scale_factor_changed();
623
624 self.winit_window.request_redraw();
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::Resized(_) = event {
635 self.rebuild_user_interface(&state, window);
636 }
637
638 if response.repaint && *event != WindowEvent::RedrawRequested {
639 self.winit_window.request_redraw();
640 }
641
642 consumed = response.consumed;
645 },
646 }
647
648 if matches!(
649 event,
650 WindowEvent::CursorMoved { .. } |
651 WindowEvent::MouseInput { .. } |
652 WindowEvent::MouseWheel { .. } |
653 WindowEvent::KeyboardInput { .. }
654 ) && window
655 .active_webview()
656 .is_some_and(|webview| self.has_active_dialog_for_webview(webview.id()))
657 {
658 consumed = true;
659 }
660
661 if !consumed {
662 if let WindowEvent::Resized(new_inner_size) = event {
664 if self.inner_size.get() != new_inner_size {
665 self.inner_size.set(new_inner_size);
666 self.window_rendering_context.resize(new_inner_size);
670 }
671 }
672
673 if let Some(webview) = window.active_webview() {
674 match event {
675 WindowEvent::KeyboardInput { event, .. } => {
676 self.handle_keyboard_input(state.clone(), window, event)
677 },
678 WindowEvent::ModifiersChanged(modifiers) => {
679 self.modifiers_state.set(modifiers.state())
680 },
681 WindowEvent::MouseInput { state, button, .. } => {
682 self.handle_mouse_button_event(&webview, button, state);
683 },
684 WindowEvent::CursorMoved { position, .. } => {
685 self.handle_mouse_move_event(&webview, position);
686 },
687 WindowEvent::CursorLeft { .. } => {
688 if webview
689 .rect()
690 .contains(self.webview_relative_mouse_point.get())
691 {
692 webview.notify_input_event(InputEvent::MouseLeftViewport(
693 MouseLeftViewportEvent::default(),
694 ));
695 }
696 },
697 WindowEvent::MouseWheel { delta, .. } => {
698 let (delta_x, delta_y, mode) = match delta {
699 MouseScrollDelta::LineDelta(delta_x, delta_y) => (
700 (delta_x * LINE_WIDTH) as f64,
701 (delta_y * LINE_HEIGHT) as f64,
702 WheelMode::DeltaLine,
703 ),
704 MouseScrollDelta::PixelDelta(delta) => {
705 (delta.x, delta.y, WheelMode::DeltaPixel)
706 },
707 };
708
709 let delta = WheelDelta {
711 x: delta_x,
712 y: delta_y,
713 z: 0.0,
714 mode,
715 };
716 let point = self.webview_relative_mouse_point.get();
717 webview.notify_input_event(InputEvent::Wheel(WheelEvent::new(
718 delta,
719 point.into(),
720 )));
721 },
722 WindowEvent::Touch(touch) => {
723 webview.notify_input_event(InputEvent::Touch(TouchEvent::new(
724 winit_phase_to_touch_event_type(touch.phase),
725 TouchId(touch.id as i32),
726 DevicePoint::new(touch.location.x as f32, touch.location.y as f32)
727 .into(),
728 )));
729 },
730 WindowEvent::PinchGesture { delta, .. } => {
731 webview.pinch_zoom(
732 delta as f32 + 1.0,
733 self.webview_relative_mouse_point.get(),
734 );
735 },
736 WindowEvent::CloseRequested => {
737 window.schedule_close();
738 },
739 WindowEvent::ThemeChanged(theme) => {
740 webview.notify_theme_change(match theme {
741 winit::window::Theme::Light => Theme::Light,
742 winit::window::Theme::Dark => Theme::Dark,
743 });
744 },
745 WindowEvent::Ime(ime) => match ime {
746 Ime::Enabled => {
747 webview.notify_input_event(InputEvent::Ime(ImeEvent::Composition(
748 servo::CompositionEvent {
749 state: servo::CompositionState::Start,
750 data: String::new(),
751 },
752 )));
753 },
754 Ime::Preedit(text, _) => {
755 webview.notify_input_event(InputEvent::Ime(ImeEvent::Composition(
756 servo::CompositionEvent {
757 state: servo::CompositionState::Update,
758 data: text,
759 },
760 )));
761 },
762 Ime::Commit(text) => {
763 webview.notify_input_event(InputEvent::Ime(ImeEvent::Composition(
764 servo::CompositionEvent {
765 state: servo::CompositionState::End,
766 data: text,
767 },
768 )));
769 },
770 Ime::Disabled => {
771 webview.notify_input_event(InputEvent::Ime(ImeEvent::Dismissed));
772 },
773 },
774 _ => {},
775 }
776 }
777 }
778 }
779
780 fn handle_winit_app_event(&self, app_event: AppEvent) {
781 if let AppEvent::Accessibility(ref event) = app_event {
782 if self
783 .gui
784 .borrow_mut()
785 .handle_accesskit_event(&event.window_event)
786 {
787 self.winit_window.request_redraw();
788 }
789 }
790 }
791
792 fn request_repaint(&self, window: &ServoShellWindow) {
793 self.winit_window.request_redraw();
794
795 if window
798 .active_webview()
799 .is_some_and(|webview| self.has_active_dialog_for_webview(webview.id()))
800 {
801 window.set_needs_repaint();
802 }
803 }
804
805 fn request_resize(&self, _: &WebView, new_outer_size: DeviceIntSize) -> Option<DeviceIntSize> {
806 let inner_size = self.winit_window.inner_size();
809 let outer_size = self.winit_window.outer_size();
810 let decoration_size: DeviceIntSize = Size2D::new(
811 outer_size.height - inner_size.height,
812 outer_size.width - inner_size.width,
813 )
814 .cast();
815
816 let screen_size = (self.screen_size.to_f32() * self.hidpi_scale_factor()).to_i32();
817 let new_outer_size =
818 new_outer_size.clamp(MIN_WINDOW_INNER_SIZE + decoration_size, screen_size * 2);
819
820 if outer_size.width == new_outer_size.width as u32 &&
821 outer_size.height == new_outer_size.height as u32
822 {
823 return Some(new_outer_size);
824 }
825
826 let new_inner_size = new_outer_size - decoration_size;
827 self.winit_window
828 .request_inner_size(PhysicalSize::new(
829 new_inner_size.width,
830 new_inner_size.height,
831 ))
832 .map(|resulting_size| {
833 DeviceIntSize::new(
834 resulting_size.width as i32 + decoration_size.width,
835 resulting_size.height as i32 + decoration_size.height,
836 )
837 })
838 }
839
840 fn window_rect(&self) -> DeviceIndependentIntRect {
841 let outer_size = self.winit_window.outer_size();
842 let scale = self.hidpi_scale_factor();
843
844 let outer_size = winit_size_to_euclid_size(outer_size).to_i32();
845
846 let origin = self
847 .winit_window
848 .outer_position()
849 .map(winit_position_to_euclid_point)
850 .unwrap_or_default();
851 convert_rect_to_css_pixel(
852 DeviceIntRect::from_origin_and_size(origin, outer_size),
853 scale,
854 )
855 }
856
857 fn set_position(&self, point: DeviceIntPoint) {
858 self.winit_window
859 .set_outer_position::<PhysicalPosition<i32>>(PhysicalPosition::new(point.x, point.y))
860 }
861
862 fn set_fullscreen(&self, state: bool) {
863 if self.fullscreen.get() != state {
864 self.winit_window.set_fullscreen(if state {
865 Some(winit::window::Fullscreen::Borderless(Some(
866 self.monitor.clone(),
867 )))
868 } else {
869 None
870 });
871 }
872 self.fullscreen.set(state);
873 }
874
875 fn get_fullscreen(&self) -> bool {
876 self.fullscreen.get()
877 }
878
879 fn set_cursor(&self, cursor: Cursor) {
880 use winit::window::CursorIcon;
881
882 let winit_cursor = match cursor {
883 Cursor::Default => CursorIcon::Default,
884 Cursor::Pointer => CursorIcon::Pointer,
885 Cursor::ContextMenu => CursorIcon::ContextMenu,
886 Cursor::Help => CursorIcon::Help,
887 Cursor::Progress => CursorIcon::Progress,
888 Cursor::Wait => CursorIcon::Wait,
889 Cursor::Cell => CursorIcon::Cell,
890 Cursor::Crosshair => CursorIcon::Crosshair,
891 Cursor::Text => CursorIcon::Text,
892 Cursor::VerticalText => CursorIcon::VerticalText,
893 Cursor::Alias => CursorIcon::Alias,
894 Cursor::Copy => CursorIcon::Copy,
895 Cursor::Move => CursorIcon::Move,
896 Cursor::NoDrop => CursorIcon::NoDrop,
897 Cursor::NotAllowed => CursorIcon::NotAllowed,
898 Cursor::Grab => CursorIcon::Grab,
899 Cursor::Grabbing => CursorIcon::Grabbing,
900 Cursor::EResize => CursorIcon::EResize,
901 Cursor::NResize => CursorIcon::NResize,
902 Cursor::NeResize => CursorIcon::NeResize,
903 Cursor::NwResize => CursorIcon::NwResize,
904 Cursor::SResize => CursorIcon::SResize,
905 Cursor::SeResize => CursorIcon::SeResize,
906 Cursor::SwResize => CursorIcon::SwResize,
907 Cursor::WResize => CursorIcon::WResize,
908 Cursor::EwResize => CursorIcon::EwResize,
909 Cursor::NsResize => CursorIcon::NsResize,
910 Cursor::NeswResize => CursorIcon::NeswResize,
911 Cursor::NwseResize => CursorIcon::NwseResize,
912 Cursor::ColResize => CursorIcon::ColResize,
913 Cursor::RowResize => CursorIcon::RowResize,
914 Cursor::AllScroll => CursorIcon::AllScroll,
915 Cursor::ZoomIn => CursorIcon::ZoomIn,
916 Cursor::ZoomOut => CursorIcon::ZoomOut,
917 Cursor::None => {
918 self.winit_window.set_cursor_visible(false);
919 return;
920 },
921 };
922 self.winit_window.set_cursor(winit_cursor);
923 self.winit_window.set_cursor_visible(true);
924 }
925
926 fn id(&self) -> ServoShellWindowId {
927 let id: u64 = self.winit_window.id().into();
928 id.into()
929 }
930
931 #[cfg(feature = "webxr")]
932 fn new_glwindow(&self, event_loop: &ActiveEventLoop) -> Rc<dyn servo::webxr::GlWindow> {
933 let size = self.winit_window.outer_size();
934
935 let window_attr = winit::window::Window::default_attributes()
936 .with_title("Servo XR".to_string())
937 .with_inner_size(size)
938 .with_visible(false);
939
940 let winit_window = event_loop
941 .create_window(window_attr)
942 .expect("Failed to create window.");
943
944 let pose = Rc::new(XRWindowPose {
945 xr_rotation: Cell::new(Rotation3D::identity()),
946 xr_translation: Cell::new(Vector3D::zero()),
947 });
948 self.xr_window_poses.borrow_mut().push(pose.clone());
949 Rc::new(XRWindow { winit_window, pose })
950 }
951
952 fn rendering_context(&self) -> Rc<dyn RenderingContext> {
953 self.rendering_context.clone()
954 }
955
956 fn theme(&self) -> servo::Theme {
957 match self.winit_window.theme() {
958 Some(winit::window::Theme::Dark) => servo::Theme::Dark,
959 Some(winit::window::Theme::Light) | None => servo::Theme::Light,
960 }
961 }
962
963 fn maximize(&self, _webview: &WebView) {
964 self.winit_window.set_maximized(true);
965 }
966
967 fn notify_input_event_handled(
969 &self,
970 webview: &WebView,
971 id: InputEventId,
972 result: InputEventResult,
973 ) {
974 let Some(keyboard_event) = self.pending_keyboard_events.borrow_mut().remove(&id) else {
975 return;
976 };
977 if result.intersects(InputEventResult::DefaultPrevented | InputEventResult::Consumed) {
978 return;
979 }
980
981 ShortcutMatcher::from_event(keyboard_event.event)
982 .shortcut(CMD_OR_CONTROL, '=', || {
983 webview.set_page_zoom(webview.page_zoom() + 0.1);
984 })
985 .shortcut(CMD_OR_CONTROL, '+', || {
986 webview.set_page_zoom(webview.page_zoom() + 0.1);
987 })
988 .shortcut(CMD_OR_CONTROL, '-', || {
989 webview.set_page_zoom(webview.page_zoom() - 0.1);
990 })
991 .shortcut(CMD_OR_CONTROL, '0', || {
992 webview.set_page_zoom(1.0);
993 });
994 }
995
996 fn focused(&self) -> bool {
997 self.winit_window.has_focus()
998 }
999
1000 fn show_embedder_control(&self, webview_id: WebViewId, embedder_control: EmbedderControl) {
1001 let control_id = embedder_control.id();
1002 match embedder_control {
1003 EmbedderControl::SelectElement(prompt) => {
1004 let offset = self.gui.borrow().toolbar_height();
1007 self.add_dialog(
1008 webview_id,
1009 Dialog::new_select_element_dialog(prompt, offset),
1010 );
1011 },
1012 EmbedderControl::ColorPicker(color_picker) => {
1013 let offset = self.gui.borrow().toolbar_height();
1016 self.add_dialog(
1017 webview_id,
1018 Dialog::new_color_picker_dialog(color_picker, offset),
1019 );
1020 },
1021 EmbedderControl::InputMethod(input_method_control) => {
1022 self.visible_input_methods.borrow_mut().push(control_id);
1023 self.show_ime(input_method_control);
1024 },
1025 EmbedderControl::FilePicker(file_picker) => {
1026 self.add_dialog(webview_id, Dialog::new_file_dialog(file_picker));
1027 },
1028 EmbedderControl::SimpleDialog(simple_dialog) => {
1029 self.add_dialog(webview_id, Dialog::new_simple_dialog(simple_dialog));
1030 },
1031 EmbedderControl::ContextMenu(prompt) => {
1032 let offset = self.gui.borrow().toolbar_height();
1033 self.add_dialog(webview_id, Dialog::new_context_menu(prompt, offset));
1034 },
1035 }
1036 }
1037
1038 fn hide_embedder_control(&self, webview_id: WebViewId, embedder_control_id: EmbedderControlId) {
1039 {
1040 let mut visible_input_methods = self.visible_input_methods.borrow_mut();
1041 if let Some(index) = visible_input_methods
1042 .iter()
1043 .position(|visible_id| *visible_id == embedder_control_id)
1044 {
1045 visible_input_methods.remove(index);
1046 self.winit_window.set_ime_allowed(false);
1047 }
1048 }
1049 self.remove_dialog(webview_id, embedder_control_id);
1050 }
1051
1052 fn show_bluetooth_device_dialog(
1053 &self,
1054 webview_id: WebViewId,
1055 devices: Vec<String>,
1056 response_sender: GenericSender<Option<String>>,
1057 ) {
1058 self.add_dialog(
1059 webview_id,
1060 Dialog::new_device_selection_dialog(devices, response_sender),
1061 );
1062 }
1063
1064 fn show_permission_dialog(&self, webview_id: WebViewId, permission_request: PermissionRequest) {
1065 self.add_dialog(
1066 webview_id,
1067 Dialog::new_permission_request_dialog(permission_request),
1068 );
1069 }
1070
1071 fn show_http_authentication_dialog(
1072 &self,
1073 webview_id: WebViewId,
1074 authentication_request: AuthenticationRequest,
1075 ) {
1076 self.add_dialog(
1077 webview_id,
1078 Dialog::new_authentication_dialog(authentication_request),
1079 );
1080 }
1081
1082 fn dismiss_embedder_controls_for_webview(&self, webview_id: WebViewId) {
1083 self.dialogs.borrow_mut().remove(&webview_id);
1084 }
1085
1086 fn take_user_interface_commands(&self) -> Vec<UserInterfaceCommand> {
1087 self.gui.borrow_mut().take_user_interface_commands()
1088 }
1089}
1090
1091fn winit_phase_to_touch_event_type(phase: TouchPhase) -> TouchEventType {
1092 match phase {
1093 TouchPhase::Started => TouchEventType::Down,
1094 TouchPhase::Moved => TouchEventType::Move,
1095 TouchPhase::Ended => TouchEventType::Up,
1096 TouchPhase::Cancelled => TouchEventType::Cancel,
1097 }
1098}
1099
1100#[cfg(any(target_os = "linux", target_os = "windows"))]
1101fn load_icon(icon_bytes: &[u8]) -> Icon {
1102 let (icon_rgba, icon_width, icon_height) = {
1103 use image::{GenericImageView, Pixel};
1104 let image = image::load_from_memory(icon_bytes).expect("Failed to load icon");
1105 let (width, height) = image.dimensions();
1106 let mut rgba = Vec::with_capacity((width * height) as usize * 4);
1107 for (_, _, pixel) in image.pixels() {
1108 rgba.extend_from_slice(&pixel.to_rgba().0);
1109 }
1110 (rgba, width, height)
1111 };
1112 Icon::from_rgba(icon_rgba, icon_width, icon_height).expect("Failed to load icon")
1113}
1114
1115#[cfg(feature = "webxr")]
1116struct XRWindow {
1117 winit_window: winit::window::Window,
1118 pose: Rc<XRWindowPose>,
1119}
1120
1121struct XRWindowPose {
1122 xr_rotation: Cell<Rotation3D<f32, UnknownUnit, UnknownUnit>>,
1123 xr_translation: Cell<Vector3D<f32, UnknownUnit>>,
1124}
1125
1126#[cfg(feature = "webxr")]
1127impl servo::webxr::GlWindow for XRWindow {
1128 fn get_render_target(
1129 &self,
1130 device: &mut surfman::Device,
1131 _context: &mut surfman::Context,
1132 ) -> servo::webxr::GlWindowRenderTarget {
1133 self.winit_window.set_visible(true);
1134 let window_handle = self
1135 .winit_window
1136 .window_handle()
1137 .expect("could not get window handle from window");
1138 let size = self.winit_window.inner_size();
1139 let size = Size2D::new(size.width as i32, size.height as i32);
1140 let native_widget = device
1141 .connection()
1142 .create_native_widget_from_window_handle(window_handle, size)
1143 .expect("Failed to create native widget");
1144 servo::webxr::GlWindowRenderTarget::NativeWidget(native_widget)
1145 }
1146
1147 fn get_rotation(&self) -> Rotation3D<f32, UnknownUnit, UnknownUnit> {
1148 self.pose.xr_rotation.get()
1149 }
1150
1151 fn get_translation(&self) -> Vector3D<f32, UnknownUnit> {
1152 self.pose.xr_translation.get()
1153 }
1154
1155 fn get_mode(&self) -> servo::webxr::GlWindowMode {
1156 use servo::pref;
1157 if pref!(dom_webxr_glwindow_red_cyan) {
1158 servo::webxr::GlWindowMode::StereoRedCyan
1159 } else if pref!(dom_webxr_glwindow_left_right) {
1160 servo::webxr::GlWindowMode::StereoLeftRight
1161 } else if pref!(dom_webxr_glwindow_spherical) {
1162 servo::webxr::GlWindowMode::Spherical
1163 } else if pref!(dom_webxr_glwindow_cubemap) {
1164 servo::webxr::GlWindowMode::Cubemap
1165 } else {
1166 servo::webxr::GlWindowMode::Blit
1167 }
1168 }
1169
1170 fn display_handle(&self) -> raw_window_handle::DisplayHandle<'_> {
1171 self.winit_window
1172 .display_handle()
1173 .expect("Every window should have a display handle")
1174 }
1175}
1176
1177impl XRWindowPose {
1178 fn handle_xr_translation(&self, input: &KeyboardEvent) {
1179 if input.event.state != KeyState::Down {
1180 return;
1181 }
1182 const NORMAL_TRANSLATE: f32 = 0.1;
1183 const QUICK_TRANSLATE: f32 = 1.0;
1184 let mut x = 0.0;
1185 let mut z = 0.0;
1186 match input.event.key {
1187 Key::Character(ref k) => match &**k {
1188 "w" => z = -NORMAL_TRANSLATE,
1189 "W" => z = -QUICK_TRANSLATE,
1190 "s" => z = NORMAL_TRANSLATE,
1191 "S" => z = QUICK_TRANSLATE,
1192 "a" => x = -NORMAL_TRANSLATE,
1193 "A" => x = -QUICK_TRANSLATE,
1194 "d" => x = NORMAL_TRANSLATE,
1195 "D" => x = QUICK_TRANSLATE,
1196 _ => return,
1197 },
1198 _ => return,
1199 };
1200 let (old_x, old_y, old_z) = self.xr_translation.get().to_tuple();
1201 let vec = Vector3D::new(x + old_x, old_y, z + old_z);
1202 self.xr_translation.set(vec);
1203 }
1204
1205 fn handle_xr_rotation(&self, input: &KeyEvent, modifiers: ModifiersState) {
1206 if input.state != ElementState::Pressed {
1207 return;
1208 }
1209 let mut x = 0.0;
1210 let mut y = 0.0;
1211 match input.logical_key {
1212 LogicalKey::Named(WinitNamedKey::ArrowUp) => x = 1.0,
1213 LogicalKey::Named(WinitNamedKey::ArrowDown) => x = -1.0,
1214 LogicalKey::Named(WinitNamedKey::ArrowLeft) => y = 1.0,
1215 LogicalKey::Named(WinitNamedKey::ArrowRight) => y = -1.0,
1216 _ => return,
1217 };
1218 if modifiers.shift_key() {
1219 x *= 10.0;
1220 y *= 10.0;
1221 }
1222 let x: Rotation3D<_, UnknownUnit, UnknownUnit> = Rotation3D::around_x(Angle::degrees(x));
1223 let y: Rotation3D<_, UnknownUnit, UnknownUnit> = Rotation3D::around_y(Angle::degrees(y));
1224 let rotation = self.xr_rotation.get().then(&x).then(&y);
1225 self.xr_rotation.set(rotation);
1226 }
1227}
1228
1229#[derive(Default)]
1230pub struct TouchEventSimulator {
1231 pub left_mouse_button_down: Cell<bool>,
1232}
1233
1234impl TouchEventSimulator {
1235 fn maybe_consume_move_button_event(
1236 &self,
1237 webview: &WebView,
1238 button: MouseButton,
1239 action: ElementState,
1240 point: DevicePoint,
1241 ) -> bool {
1242 if button != MouseButton::Left {
1243 return false;
1244 }
1245
1246 if action == ElementState::Pressed && !self.left_mouse_button_down.get() {
1247 webview.notify_input_event(InputEvent::Touch(TouchEvent::new(
1248 TouchEventType::Down,
1249 TouchId(0),
1250 point.into(),
1251 )));
1252 self.left_mouse_button_down.set(true);
1253 } else if action == ElementState::Released {
1254 webview.notify_input_event(InputEvent::Touch(TouchEvent::new(
1255 TouchEventType::Up,
1256 TouchId(0),
1257 point.into(),
1258 )));
1259 self.left_mouse_button_down.set(false);
1260 }
1261
1262 true
1263 }
1264
1265 fn maybe_consume_mouse_move_event(
1266 &self,
1267 webview: &WebView,
1268 point: Point2D<f32, DevicePixel>,
1269 ) -> bool {
1270 if !self.left_mouse_button_down.get() {
1271 return false;
1272 }
1273
1274 webview.notify_input_event(InputEvent::Touch(TouchEvent::new(
1275 TouchEventType::Move,
1276 TouchId(0),
1277 point.into(),
1278 )));
1279 true
1280 }
1281}