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