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: &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::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: &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, '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.width - inner_size.width,
876 outer_size.height - inner_size.height,
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 if self.inner_size.get() != resulting_size {
900 self.inner_size.set(resulting_size);
901 self.window_rendering_context.resize(resulting_size);
902 }
903
904 DeviceIntSize::new(
905 resulting_size.width as i32 + decoration_size.width,
906 resulting_size.height as i32 + decoration_size.height,
907 )
908 })
909 }
910
911 fn window_rect(&self) -> DeviceIndependentIntRect {
912 let outer_size = self.winit_window.outer_size();
913 let scale = self.hidpi_scale_factor();
914
915 let outer_size = winit_size_to_euclid_size(outer_size).to_i32();
916
917 let origin = self
918 .winit_window
919 .outer_position()
920 .map(winit_position_to_euclid_point)
921 .unwrap_or_default();
922 convert_rect_to_css_pixel(
923 DeviceIntRect::from_origin_and_size(origin, outer_size),
924 scale,
925 )
926 }
927
928 fn set_position(&self, point: DeviceIntPoint) {
929 self.winit_window
930 .set_outer_position::<PhysicalPosition<i32>>(PhysicalPosition::new(point.x, point.y))
931 }
932
933 fn set_fullscreen(&self, state: bool) {
934 let monitor = self
935 .winit_window()
936 .current_monitor()
937 .or_else(|| self.winit_window.available_monitors().nth(0))
938 .expect("No monitor detected");
939 if self.fullscreen.get() != state {
940 self.winit_window.set_fullscreen(if state {
941 Some(winit::window::Fullscreen::Borderless(Some(monitor)))
942 } else {
943 None
944 });
945 }
946 self.fullscreen.set(state);
947 }
948
949 fn get_fullscreen(&self) -> bool {
950 self.fullscreen.get()
951 }
952
953 fn set_cursor(&self, cursor: Cursor) {
954 use winit::window::CursorIcon;
955
956 let winit_cursor = match cursor {
957 Cursor::Default => CursorIcon::Default,
958 Cursor::Pointer => CursorIcon::Pointer,
959 Cursor::ContextMenu => CursorIcon::ContextMenu,
960 Cursor::Help => CursorIcon::Help,
961 Cursor::Progress => CursorIcon::Progress,
962 Cursor::Wait => CursorIcon::Wait,
963 Cursor::Cell => CursorIcon::Cell,
964 Cursor::Crosshair => CursorIcon::Crosshair,
965 Cursor::Text => CursorIcon::Text,
966 Cursor::VerticalText => CursorIcon::VerticalText,
967 Cursor::Alias => CursorIcon::Alias,
968 Cursor::Copy => CursorIcon::Copy,
969 Cursor::Move => CursorIcon::Move,
970 Cursor::NoDrop => CursorIcon::NoDrop,
971 Cursor::NotAllowed => CursorIcon::NotAllowed,
972 Cursor::Grab => CursorIcon::Grab,
973 Cursor::Grabbing => CursorIcon::Grabbing,
974 Cursor::EResize => CursorIcon::EResize,
975 Cursor::NResize => CursorIcon::NResize,
976 Cursor::NeResize => CursorIcon::NeResize,
977 Cursor::NwResize => CursorIcon::NwResize,
978 Cursor::SResize => CursorIcon::SResize,
979 Cursor::SeResize => CursorIcon::SeResize,
980 Cursor::SwResize => CursorIcon::SwResize,
981 Cursor::WResize => CursorIcon::WResize,
982 Cursor::EwResize => CursorIcon::EwResize,
983 Cursor::NsResize => CursorIcon::NsResize,
984 Cursor::NeswResize => CursorIcon::NeswResize,
985 Cursor::NwseResize => CursorIcon::NwseResize,
986 Cursor::ColResize => CursorIcon::ColResize,
987 Cursor::RowResize => CursorIcon::RowResize,
988 Cursor::AllScroll => CursorIcon::AllScroll,
989 Cursor::ZoomIn => CursorIcon::ZoomIn,
990 Cursor::ZoomOut => CursorIcon::ZoomOut,
991 Cursor::None => {
992 self.winit_window.set_cursor_visible(false);
993 return;
994 },
995 };
996 self.winit_window.set_cursor(winit_cursor);
997 self.winit_window.set_cursor_visible(true);
998 }
999
1000 fn id(&self) -> ServoShellWindowId {
1001 let id: u64 = self.winit_window.id().into();
1002 id.into()
1003 }
1004
1005 #[cfg(feature = "webxr")]
1006 fn new_glwindow(&self, event_loop: &ActiveEventLoop) -> Rc<dyn servo::webxr::GlWindow> {
1007 let size = self.winit_window.outer_size();
1008
1009 let window_attr = winit::window::Window::default_attributes()
1010 .with_title("Servo XR".to_string())
1011 .with_inner_size(size)
1012 .with_visible(false);
1013
1014 let winit_window = event_loop
1015 .create_window(window_attr)
1016 .expect("Failed to create window.");
1017
1018 let pose = Rc::new(XRWindowPose {
1019 xr_rotation: Cell::new(Rotation3D::identity()),
1020 xr_translation: Cell::new(Vector3D::zero()),
1021 });
1022 self.xr_window_poses.borrow_mut().push(pose.clone());
1023 Rc::new(XRWindow { winit_window, pose })
1024 }
1025
1026 fn rendering_context(&self) -> Rc<dyn RenderingContext> {
1027 self.rendering_context.clone()
1028 }
1029
1030 fn theme(&self) -> servo::Theme {
1031 match self.winit_window.theme() {
1032 Some(winit::window::Theme::Dark) => servo::Theme::Dark,
1033 Some(winit::window::Theme::Light) | None => servo::Theme::Light,
1034 }
1035 }
1036
1037 fn maximize(&self, _webview: &WebView) {
1038 self.winit_window.set_maximized(true);
1039 }
1040
1041 fn notify_input_event_handled(
1043 &self,
1044 webview: &WebView,
1045 id: InputEventId,
1046 result: InputEventResult,
1047 ) {
1048 let Some(keyboard_event) = self.pending_keyboard_events.borrow_mut().remove(&id) else {
1049 return;
1050 };
1051 if result.intersects(InputEventResult::DefaultPrevented | InputEventResult::Consumed) {
1052 return;
1053 }
1054
1055 ShortcutMatcher::from_event(keyboard_event.event)
1056 .shortcut(CMD_OR_CONTROL, '=', || {
1057 webview.set_page_zoom(webview.page_zoom() + 0.1);
1058 })
1059 .shortcut(CMD_OR_CONTROL, '+', || {
1060 webview.set_page_zoom(webview.page_zoom() + 0.1);
1061 })
1062 .shortcut(CMD_OR_CONTROL, '-', || {
1063 webview.set_page_zoom(webview.page_zoom() - 0.1);
1064 })
1065 .shortcut(CMD_OR_CONTROL, '0', || {
1066 webview.set_page_zoom(1.0);
1067 })
1068 .shortcut(CMD_OR_CONTROL, 'R', || webview.reload())
1069 .shortcut(Modifiers::empty(), Key::Named(NamedKey::F5), || {
1070 webview.reload()
1071 });
1072 }
1073
1074 fn focus(&self) {
1075 self.winit_window.focus_window();
1076 }
1077
1078 fn has_platform_focus(&self) -> bool {
1079 self.winit_window.has_focus()
1080 }
1081
1082 fn show_embedder_control(&self, webview_id: WebViewId, embedder_control: EmbedderControl) {
1083 let control_id = embedder_control.id();
1084 match embedder_control {
1085 EmbedderControl::SelectElement(prompt) => {
1086 let offset = self.gui.borrow().toolbar_height();
1089 self.add_dialog(
1090 webview_id,
1091 Dialog::new_select_element_dialog(prompt, offset),
1092 );
1093 },
1094 EmbedderControl::ColorPicker(color_picker) => {
1095 let offset = self.gui.borrow().toolbar_height();
1098 self.add_dialog(
1099 webview_id,
1100 Dialog::new_color_picker_dialog(color_picker, offset),
1101 );
1102 },
1103 EmbedderControl::InputMethod(input_method_control) => {
1104 self.show_ime(control_id, input_method_control);
1105 },
1106 EmbedderControl::FilePicker(file_picker) => {
1107 self.add_dialog(webview_id, Dialog::new_file_dialog(file_picker));
1108 },
1109 EmbedderControl::SimpleDialog(simple_dialog) => {
1110 self.add_dialog(webview_id, Dialog::new_simple_dialog(simple_dialog));
1111 },
1112 EmbedderControl::ContextMenu(prompt) => {
1113 let offset = self.gui.borrow().toolbar_height();
1114 self.add_dialog(webview_id, Dialog::new_context_menu(prompt, offset));
1115 },
1116 }
1117 }
1118
1119 fn hide_embedder_control(&self, webview_id: WebViewId, embedder_control_id: EmbedderControlId) {
1120 if self.visible_input_method.get() == Some(embedder_control_id) {
1121 self.visible_input_method.set(None);
1122 self.winit_window.set_ime_allowed(false);
1123 return;
1124 }
1125 self.remove_dialog(webview_id, embedder_control_id);
1126 }
1127
1128 fn show_bluetooth_device_dialog(
1129 &self,
1130 webview_id: WebViewId,
1131 request: BluetoothDeviceSelectionRequest,
1132 ) {
1133 self.add_dialog(webview_id, Dialog::new_device_selection_dialog(request));
1134 }
1135
1136 fn show_permission_dialog(&self, webview_id: WebViewId, permission_request: PermissionRequest) {
1137 self.add_dialog(
1138 webview_id,
1139 Dialog::new_permission_request_dialog(permission_request),
1140 );
1141 }
1142
1143 fn show_http_authentication_dialog(
1144 &self,
1145 webview_id: WebViewId,
1146 authentication_request: AuthenticationRequest,
1147 ) {
1148 self.add_dialog(
1149 webview_id,
1150 Dialog::new_authentication_dialog(authentication_request),
1151 );
1152 }
1153
1154 fn dismiss_embedder_controls_for_webview(&self, webview_id: WebViewId) {
1155 self.dialogs.borrow_mut().remove(&webview_id);
1156 }
1157
1158 fn show_console_message(&self, level: servo::ConsoleLogLevel, message: &str) {
1159 println!("{message}");
1160 log::log!(level.into(), "{message}");
1161 }
1162
1163 fn notify_accessibility_tree_update(
1164 &self,
1165 _webview: WebView,
1166 tree_update: accesskit::TreeUpdate,
1167 ) {
1168 self.gui
1169 .borrow_mut()
1170 .notify_accessibility_tree_update(tree_update);
1171 }
1172}
1173
1174fn winit_phase_to_touch_event_type(phase: TouchPhase) -> TouchEventType {
1175 match phase {
1176 TouchPhase::Started => TouchEventType::Down,
1177 TouchPhase::Moved => TouchEventType::Move,
1178 TouchPhase::Ended => TouchEventType::Up,
1179 TouchPhase::Cancelled => TouchEventType::Cancel,
1180 }
1181}
1182
1183#[cfg(any(target_os = "linux", target_os = "windows"))]
1184fn load_icon(icon_bytes: &[u8]) -> Icon {
1185 let (icon_rgba, icon_width, icon_height) = {
1186 use image::{GenericImageView, Pixel};
1187 let image = image::load_from_memory(icon_bytes).expect("Failed to load icon");
1188 let (width, height) = image.dimensions();
1189 let mut rgba = Vec::with_capacity((width * height) as usize * 4);
1190 for (_, _, pixel) in image.pixels() {
1191 rgba.extend_from_slice(&pixel.to_rgba().0);
1192 }
1193 (rgba, width, height)
1194 };
1195 Icon::from_rgba(icon_rgba, icon_width, icon_height).expect("Failed to load icon")
1196}
1197
1198#[cfg(feature = "webxr")]
1199struct XRWindow {
1200 winit_window: winit::window::Window,
1201 pose: Rc<XRWindowPose>,
1202}
1203
1204struct XRWindowPose {
1205 xr_rotation: Cell<Rotation3D<f32, UnknownUnit, UnknownUnit>>,
1206 xr_translation: Cell<Vector3D<f32, UnknownUnit>>,
1207}
1208
1209#[cfg(feature = "webxr")]
1210impl servo::webxr::GlWindow for XRWindow {
1211 fn get_render_target(
1212 &self,
1213 device: &mut surfman::Device,
1214 _context: &mut surfman::Context,
1215 ) -> servo::webxr::GlWindowRenderTarget {
1216 self.winit_window.set_visible(true);
1217 let window_handle = self
1218 .winit_window
1219 .window_handle()
1220 .expect("could not get window handle from window");
1221 let size = self.winit_window.inner_size();
1222 let size = Size2D::new(size.width as i32, size.height as i32);
1223 let native_widget = device
1224 .connection()
1225 .create_native_widget_from_window_handle(window_handle, size)
1226 .expect("Failed to create native widget");
1227 servo::webxr::GlWindowRenderTarget::NativeWidget(native_widget)
1228 }
1229
1230 fn get_rotation(&self) -> Rotation3D<f32, UnknownUnit, UnknownUnit> {
1231 self.pose.xr_rotation.get()
1232 }
1233
1234 fn get_translation(&self) -> Vector3D<f32, UnknownUnit> {
1235 self.pose.xr_translation.get()
1236 }
1237
1238 fn get_mode(&self) -> servo::webxr::GlWindowMode {
1239 use servo::pref;
1240 if pref!(dom_webxr_glwindow_red_cyan) {
1241 servo::webxr::GlWindowMode::StereoRedCyan
1242 } else if pref!(dom_webxr_glwindow_left_right) {
1243 servo::webxr::GlWindowMode::StereoLeftRight
1244 } else if pref!(dom_webxr_glwindow_spherical) {
1245 servo::webxr::GlWindowMode::Spherical
1246 } else if pref!(dom_webxr_glwindow_cubemap) {
1247 servo::webxr::GlWindowMode::Cubemap
1248 } else {
1249 servo::webxr::GlWindowMode::Blit
1250 }
1251 }
1252
1253 fn display_handle(&self) -> raw_window_handle::DisplayHandle<'_> {
1254 self.winit_window
1255 .display_handle()
1256 .expect("Every window should have a display handle")
1257 }
1258}
1259
1260impl XRWindowPose {
1261 fn handle_xr_translation(&self, input: &KeyboardEvent) {
1262 if input.event.state != KeyState::Down {
1263 return;
1264 }
1265 const NORMAL_TRANSLATE: f32 = 0.1;
1266 const QUICK_TRANSLATE: f32 = 1.0;
1267 let mut x = 0.0;
1268 let mut z = 0.0;
1269 match input.event.key {
1270 Key::Character(ref k) => match &**k {
1271 "w" => z = -NORMAL_TRANSLATE,
1272 "W" => z = -QUICK_TRANSLATE,
1273 "s" => z = NORMAL_TRANSLATE,
1274 "S" => z = QUICK_TRANSLATE,
1275 "a" => x = -NORMAL_TRANSLATE,
1276 "A" => x = -QUICK_TRANSLATE,
1277 "d" => x = NORMAL_TRANSLATE,
1278 "D" => x = QUICK_TRANSLATE,
1279 _ => return,
1280 },
1281 _ => return,
1282 };
1283 let (old_x, old_y, old_z) = self.xr_translation.get().to_tuple();
1284 let vec = Vector3D::new(x + old_x, old_y, z + old_z);
1285 self.xr_translation.set(vec);
1286 }
1287
1288 fn handle_xr_rotation(&self, input: &KeyEvent, modifiers: ModifiersState) {
1289 if input.state != ElementState::Pressed {
1290 return;
1291 }
1292 let mut x = 0.0;
1293 let mut y = 0.0;
1294 match input.logical_key {
1295 LogicalKey::Named(WinitNamedKey::ArrowUp) => x = 1.0,
1296 LogicalKey::Named(WinitNamedKey::ArrowDown) => x = -1.0,
1297 LogicalKey::Named(WinitNamedKey::ArrowLeft) => y = 1.0,
1298 LogicalKey::Named(WinitNamedKey::ArrowRight) => y = -1.0,
1299 _ => return,
1300 };
1301 if modifiers.shift_key() {
1302 x *= 10.0;
1303 y *= 10.0;
1304 }
1305 let x: Rotation3D<_, UnknownUnit, UnknownUnit> = Rotation3D::around_x(Angle::degrees(x));
1306 let y: Rotation3D<_, UnknownUnit, UnknownUnit> = Rotation3D::around_y(Angle::degrees(y));
1307 let rotation = self.xr_rotation.get().then(&x).then(&y);
1308 self.xr_rotation.set(rotation);
1309 }
1310}
1311
1312#[derive(Default)]
1313pub struct TouchEventSimulator {
1314 pub left_mouse_button_down: Cell<bool>,
1315}
1316
1317impl TouchEventSimulator {
1318 fn maybe_consume_move_button_event(
1319 &self,
1320 webview: &WebView,
1321 button: MouseButton,
1322 action: ElementState,
1323 point: DevicePoint,
1324 ) -> bool {
1325 if button != MouseButton::Left {
1326 return false;
1327 }
1328
1329 if action == ElementState::Pressed && !self.left_mouse_button_down.get() {
1330 webview.notify_input_event(InputEvent::Touch(TouchEvent::new(
1331 TouchEventType::Down,
1332 TouchId(0),
1333 point.into(),
1334 TouchPointerType::Touch,
1335 )));
1336 self.left_mouse_button_down.set(true);
1337 } else if action == ElementState::Released {
1338 webview.notify_input_event(InputEvent::Touch(TouchEvent::new(
1339 TouchEventType::Up,
1340 TouchId(0),
1341 point.into(),
1342 TouchPointerType::Touch,
1343 )));
1344 self.left_mouse_button_down.set(false);
1345 }
1346
1347 true
1348 }
1349
1350 fn maybe_consume_mouse_move_event(
1351 &self,
1352 webview: &WebView,
1353 point: Point2D<f32, DevicePixel>,
1354 ) -> bool {
1355 if !self.left_mouse_button_down.get() {
1356 return false;
1357 }
1358
1359 webview.notify_input_event(InputEvent::Touch(TouchEvent::new(
1360 TouchEventType::Move,
1361 TouchId(0),
1362 point.into(),
1363 TouchPointerType::Touch,
1364 )));
1365 true
1366 }
1367}