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;
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 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 rendering_context: Rc<OffscreenRenderingContext>,
78 window_rendering_context: Rc<WindowRenderingContext>,
82 touch_event_simulator: Option<TouchEventSimulator>,
85 pending_keyboard_events: RefCell<HashMap<InputEventId, KeyboardEvent>>,
89
90 winit_window: winit::window::Window,
94
95 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 .with_visible(false);
119
120 #[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 {
168 let details = window_rendering_context.surfman_details();
169 setup_gl_accelerated_media(details.0, details.1);
170 }
171
172 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 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 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 fn handle_mouse_button_event(
226 &self,
227 webview: &WebView,
228 button: MouseButton,
229 action: ElementState,
230 ) {
231 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 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 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 .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 .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 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 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 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 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 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 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}