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