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