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