1use bevy_a11y::AccessibilityRequested;
2use bevy_ecs::entity::Entity;
3
4use bevy_ecs::entity::EntityHashMap;
5use bevy_platform::collections::HashMap;
6use bevy_window::{
7 CursorGrabMode, CursorOptions, MonitorSelection, VideoModeSelection, Window, WindowMode,
8 WindowPosition, WindowResolution, WindowWrapper,
9};
10use tracing::warn;
11
12use winit::{
13 dpi::{LogicalSize, PhysicalPosition},
14 error::ExternalError,
15 event_loop::ActiveEventLoop,
16 monitor::{MonitorHandle, VideoModeHandle},
17 window::{CursorGrabMode as WinitCursorGrabMode, Fullscreen, Window as WinitWindow, WindowId},
18};
19
20use crate::{
21 accessibility::{
22 prepare_accessibility_for_window, AccessKitAdapters, WinitActionRequestHandlers,
23 },
24 converters::{convert_enabled_buttons, convert_window_level, convert_window_theme},
25 winit_monitors::WinitMonitors,
26};
27
28#[derive(Debug, Default)]
31pub struct WinitWindows {
32 pub windows: HashMap<WindowId, WindowWrapper<WinitWindow>>,
34 pub entity_to_winit: EntityHashMap<WindowId>,
36 pub winit_to_entity: HashMap<WindowId, Entity>,
38 _not_send_sync: core::marker::PhantomData<*const ()>,
42}
43
44impl WinitWindows {
45 pub const fn new() -> Self {
47 Self {
48 windows: HashMap::new(),
49 entity_to_winit: EntityHashMap::new(),
50 winit_to_entity: HashMap::new(),
51 _not_send_sync: core::marker::PhantomData,
52 }
53 }
54
55 pub fn create_window(
57 &mut self,
58 event_loop: &ActiveEventLoop,
59 entity: Entity,
60 window: &Window,
61 cursor_options: &CursorOptions,
62 adapters: &mut AccessKitAdapters,
63 handlers: &mut WinitActionRequestHandlers,
64 accessibility_requested: &AccessibilityRequested,
65 monitors: &WinitMonitors,
66 ) -> &WindowWrapper<WinitWindow> {
67 let mut winit_window_attributes = WinitWindow::default_attributes();
68
69 winit_window_attributes = winit_window_attributes.with_visible(false);
72
73 let maybe_selected_monitor = &match window.mode {
74 WindowMode::BorderlessFullscreen(monitor_selection)
75 | WindowMode::Fullscreen(monitor_selection, _) => select_monitor(
76 monitors,
77 event_loop.primary_monitor(),
78 None,
79 &monitor_selection,
80 ),
81 WindowMode::Windowed => None,
82 };
83
84 winit_window_attributes = match window.mode {
85 WindowMode::BorderlessFullscreen(_) => winit_window_attributes
86 .with_fullscreen(Some(Fullscreen::Borderless(maybe_selected_monitor.clone()))),
87 WindowMode::Fullscreen(monitor_selection, video_mode_selection) => {
88 winit_window_attributes.with_fullscreen(Some(resolve_exclusive_fullscreen(
89 maybe_selected_monitor.clone(),
90 monitor_selection,
91 video_mode_selection,
92 )))
93 }
94 WindowMode::Windowed => {
95 if let Some(position) = winit_window_position(
96 &window.position,
97 &window.resolution,
98 monitors,
99 event_loop.primary_monitor(),
100 None,
101 ) {
102 winit_window_attributes = winit_window_attributes.with_position(position);
103 }
104 let logical_size = LogicalSize::new(window.width(), window.height());
105 if let Some(sf) = window.resolution.scale_factor_override() {
106 let inner_size = logical_size.to_physical::<f64>(sf.into());
107 winit_window_attributes.with_inner_size(inner_size)
108 } else {
109 winit_window_attributes.with_inner_size(logical_size)
110 }
111 }
112 };
113
114 winit_window_attributes = winit_window_attributes
118 .with_window_level(convert_window_level(window.window_level))
119 .with_theme(window.window_theme.map(convert_window_theme))
120 .with_resizable(window.resizable)
121 .with_enabled_buttons(convert_enabled_buttons(window.enabled_buttons))
122 .with_decorations(window.decorations)
123 .with_transparent(window.transparent)
124 .with_active(window.focused);
125
126 #[cfg(target_os = "windows")]
127 {
128 use winit::platform::windows::WindowAttributesExtWindows;
129 winit_window_attributes =
130 winit_window_attributes.with_skip_taskbar(window.skip_taskbar);
131 winit_window_attributes =
132 winit_window_attributes.with_clip_children(window.clip_children);
133 }
134
135 #[cfg(target_os = "macos")]
136 {
137 use winit::platform::macos::WindowAttributesExtMacOS;
138 winit_window_attributes = winit_window_attributes
139 .with_movable_by_window_background(window.movable_by_window_background)
140 .with_fullsize_content_view(window.fullsize_content_view)
141 .with_has_shadow(window.has_shadow)
142 .with_titlebar_hidden(!window.titlebar_shown)
143 .with_titlebar_transparent(window.titlebar_transparent)
144 .with_title_hidden(!window.titlebar_show_title)
145 .with_titlebar_buttons_hidden(!window.titlebar_show_buttons)
146 .with_borderless_game(window.borderless_game);
147 }
148
149 #[cfg(target_os = "ios")]
150 {
151 use crate::converters::convert_screen_edge;
152 use winit::platform::ios::WindowAttributesExtIOS;
153
154 let preferred_edge =
155 convert_screen_edge(window.preferred_screen_edges_deferring_system_gestures);
156
157 winit_window_attributes = winit_window_attributes
158 .with_preferred_screen_edges_deferring_system_gestures(preferred_edge);
159 winit_window_attributes = winit_window_attributes
160 .with_prefers_home_indicator_hidden(window.prefers_home_indicator_hidden);
161 winit_window_attributes = winit_window_attributes
162 .with_prefers_status_bar_hidden(window.prefers_status_bar_hidden);
163 }
164
165 let display_info = DisplayInfo {
166 window_physical_resolution: (
167 window.resolution.physical_width(),
168 window.resolution.physical_height(),
169 ),
170 window_logical_resolution: (window.resolution.width(), window.resolution.height()),
171 monitor_name: maybe_selected_monitor
172 .as_ref()
173 .and_then(MonitorHandle::name),
174 scale_factor: maybe_selected_monitor
175 .as_ref()
176 .map(MonitorHandle::scale_factor),
177 refresh_rate_millihertz: maybe_selected_monitor
178 .as_ref()
179 .and_then(MonitorHandle::refresh_rate_millihertz),
180 };
181 bevy_log::debug!("{display_info}");
182
183 #[cfg(any(
184 all(
185 any(feature = "wayland", feature = "x11"),
186 any(
187 target_os = "linux",
188 target_os = "dragonfly",
189 target_os = "freebsd",
190 target_os = "netbsd",
191 target_os = "openbsd",
192 )
193 ),
194 target_os = "windows"
195 ))]
196 if let Some(name) = &window.name {
197 #[cfg(all(
198 feature = "wayland",
199 any(
200 target_os = "linux",
201 target_os = "dragonfly",
202 target_os = "freebsd",
203 target_os = "netbsd",
204 target_os = "openbsd"
205 )
206 ))]
207 {
208 winit_window_attributes =
209 winit::platform::wayland::WindowAttributesExtWayland::with_name(
210 winit_window_attributes,
211 name.clone(),
212 "",
213 );
214 }
215
216 #[cfg(all(
217 feature = "x11",
218 any(
219 target_os = "linux",
220 target_os = "dragonfly",
221 target_os = "freebsd",
222 target_os = "netbsd",
223 target_os = "openbsd"
224 )
225 ))]
226 {
227 winit_window_attributes = winit::platform::x11::WindowAttributesExtX11::with_name(
228 winit_window_attributes,
229 name.clone(),
230 "",
231 );
232 }
233 #[cfg(target_os = "windows")]
234 {
235 winit_window_attributes =
236 winit::platform::windows::WindowAttributesExtWindows::with_class_name(
237 winit_window_attributes,
238 name.clone(),
239 );
240 }
241 }
242
243 let constraints = window.resize_constraints.check_constraints();
244 let min_inner_size = LogicalSize {
245 width: constraints.min_width,
246 height: constraints.min_height,
247 };
248 let max_inner_size = LogicalSize {
249 width: constraints.max_width,
250 height: constraints.max_height,
251 };
252
253 let winit_window_attributes =
254 if constraints.max_width.is_finite() && constraints.max_height.is_finite() {
255 winit_window_attributes
256 .with_min_inner_size(min_inner_size)
257 .with_max_inner_size(max_inner_size)
258 } else {
259 winit_window_attributes.with_min_inner_size(min_inner_size)
260 };
261
262 #[expect(clippy::allow_attributes, reason = "`unused_mut` is not always linted")]
263 #[allow(
264 unused_mut,
265 reason = "This variable needs to be mutable if `cfg(target_arch = \"wasm32\")`"
266 )]
267 let mut winit_window_attributes = winit_window_attributes.with_title(window.title.as_str());
268
269 #[cfg(target_arch = "wasm32")]
270 {
271 use wasm_bindgen::JsCast;
272 use winit::platform::web::WindowAttributesExtWebSys;
273
274 if let Some(selector) = &window.canvas {
275 let window = web_sys::window().unwrap();
276 let document = window.document().unwrap();
277 let canvas = document
278 .query_selector(selector)
279 .expect("Cannot query for canvas element.");
280 if let Some(canvas) = canvas {
281 let canvas = canvas.dyn_into::<web_sys::HtmlCanvasElement>().ok();
282 winit_window_attributes = winit_window_attributes.with_canvas(canvas);
283 } else {
284 panic!("Cannot find element: {selector}.");
285 }
286 }
287
288 winit_window_attributes =
289 winit_window_attributes.with_prevent_default(window.prevent_default_event_handling);
290 winit_window_attributes = winit_window_attributes.with_append(true);
291 }
292
293 let winit_window = event_loop.create_window(winit_window_attributes).unwrap();
294 let name = window.title.clone();
295 prepare_accessibility_for_window(
296 event_loop,
297 &winit_window,
298 entity,
299 name,
300 accessibility_requested.clone(),
301 adapters,
302 handlers,
303 );
304
305 winit_window.set_visible(window.visible);
308
309 if cursor_options.grab_mode != CursorGrabMode::None {
311 let _ = attempt_grab(&winit_window, cursor_options.grab_mode);
312 }
313
314 winit_window.set_cursor_visible(cursor_options.visible);
315
316 if !cursor_options.hit_test
319 && let Err(err) = winit_window.set_cursor_hittest(cursor_options.hit_test)
320 {
321 warn!(
322 "Could not set cursor hit test for window {}: {}",
323 window.title, err
324 );
325 }
326
327 self.entity_to_winit.insert(entity, winit_window.id());
328 self.winit_to_entity.insert(winit_window.id(), entity);
329
330 self.windows
331 .entry(winit_window.id())
332 .insert(WindowWrapper::new(winit_window))
333 .into_mut()
334 }
335
336 pub fn get_window(&self, entity: Entity) -> Option<&WindowWrapper<WinitWindow>> {
338 self.entity_to_winit
339 .get(&entity)
340 .and_then(|winit_id| self.windows.get(winit_id))
341 }
342
343 pub fn get_window_entity(&self, winit_id: WindowId) -> Option<Entity> {
347 self.winit_to_entity.get(&winit_id).cloned()
348 }
349
350 pub fn remove_window(&mut self, entity: Entity) -> Option<WindowWrapper<WinitWindow>> {
354 let winit_id = self.entity_to_winit.remove(&entity)?;
355 self.winit_to_entity.remove(&winit_id);
356 self.windows.remove(&winit_id)
357 }
358}
359
360pub fn get_selected_videomode(
363 monitor: &MonitorHandle,
364 selection: &VideoModeSelection,
365) -> Option<VideoModeHandle> {
366 match selection {
367 VideoModeSelection::Current => get_current_videomode(monitor),
368 VideoModeSelection::Specific(specified) => monitor.video_modes().find(|mode| {
369 mode.size().width == specified.physical_size.x
370 && mode.size().height == specified.physical_size.y
371 && mode.refresh_rate_millihertz() == specified.refresh_rate_millihertz
372 && mode.bit_depth() == specified.bit_depth
373 }),
374 }
375}
376
377pub(crate) fn resolve_exclusive_fullscreen(
382 monitor: Option<MonitorHandle>,
383 monitor_selection: MonitorSelection,
384 video_mode_selection: VideoModeSelection,
385) -> Fullscreen {
386 let video_mode = monitor
387 .as_ref()
388 .and_then(|m| get_selected_videomode(m, &video_mode_selection));
389 if let Some(video_mode) = video_mode {
390 return Fullscreen::Exclusive(video_mode);
391 }
392 if monitor.is_none() {
393 warn!(
394 "Could not find monitor for {:?}; falling back to borderless fullscreen",
395 monitor_selection
396 );
397 } else {
398 warn!(
399 "Could not find valid fullscreen video mode for {:?} {:?}; falling back to borderless fullscreen",
400 monitor_selection, video_mode_selection
401 );
402 }
403 Fullscreen::Borderless(monitor)
404}
405
406fn get_current_videomode(monitor: &MonitorHandle) -> Option<VideoModeHandle> {
411 monitor
412 .video_modes()
413 .filter(|mode| {
414 mode.size() == monitor.size()
415 && Some(mode.refresh_rate_millihertz()) == monitor.refresh_rate_millihertz()
416 })
417 .max_by_key(VideoModeHandle::bit_depth)
418}
419
420#[cfg(target_arch = "wasm32")]
421fn pointer_supported() -> Result<bool, ExternalError> {
422 Ok(js_sys::Reflect::has(
423 web_sys::window()
424 .ok_or(ExternalError::Ignored)?
425 .document()
426 .ok_or(ExternalError::Ignored)?
427 .as_ref(),
428 &"exitPointerLock".into(),
429 )
430 .unwrap_or(false))
431}
432
433pub(crate) fn attempt_grab(
434 winit_window: &WinitWindow,
435 grab_mode: CursorGrabMode,
436) -> Result<(), ExternalError> {
437 #[cfg(target_arch = "wasm32")]
439 if !pointer_supported()? {
440 return Err(ExternalError::Ignored);
441 }
442
443 let grab_result = match grab_mode {
444 CursorGrabMode::None => winit_window.set_cursor_grab(WinitCursorGrabMode::None),
445 CursorGrabMode::Confined => winit_window
446 .set_cursor_grab(WinitCursorGrabMode::Confined)
447 .or_else(|_e| winit_window.set_cursor_grab(WinitCursorGrabMode::None)),
448 CursorGrabMode::Locked => winit_window
449 .set_cursor_grab(WinitCursorGrabMode::Locked)
450 .or_else(|_e| winit_window.set_cursor_grab(WinitCursorGrabMode::Confined)),
451 };
452
453 if let Err(err) = grab_result {
454 let err_desc = match grab_mode {
455 CursorGrabMode::Confined | CursorGrabMode::Locked => "grab",
456 CursorGrabMode::None => "ungrab",
457 };
458
459 tracing::error!("Unable to {} cursor: {}", err_desc, err);
460 Err(err)
461 } else {
462 Ok(())
463 }
464}
465
466pub fn winit_window_position(
470 position: &WindowPosition,
471 resolution: &WindowResolution,
472 monitors: &WinitMonitors,
473 primary_monitor: Option<MonitorHandle>,
474 current_monitor: Option<MonitorHandle>,
475) -> Option<PhysicalPosition<i32>> {
476 match position {
477 WindowPosition::Automatic => {
478 None
480 }
481 WindowPosition::Centered(monitor_selection) => {
482 let maybe_monitor = select_monitor(
483 monitors,
484 primary_monitor,
485 current_monitor,
486 monitor_selection,
487 );
488
489 if let Some(monitor) = maybe_monitor {
490 let screen_size = monitor.size();
491
492 let scale_factor = match resolution.scale_factor_override() {
493 Some(scale_factor_override) => scale_factor_override as f64,
494 None => monitor.scale_factor(),
497 };
498
499 let (width, height): (u32, u32) =
501 LogicalSize::new(resolution.width(), resolution.height())
502 .to_physical::<u32>(scale_factor)
503 .into();
504
505 let position = PhysicalPosition {
506 x: screen_size.width.saturating_sub(width) as f64 / 2.
507 + monitor.position().x as f64,
508 y: screen_size.height.saturating_sub(height) as f64 / 2.
509 + monitor.position().y as f64,
510 };
511
512 Some(position.cast::<i32>())
513 } else {
514 warn!("Couldn't get monitor selected with: {monitor_selection:?}");
515 None
516 }
517 }
518 WindowPosition::At(position) => {
519 Some(PhysicalPosition::new(position[0] as f64, position[1] as f64).cast::<i32>())
520 }
521 }
522}
523
524pub fn select_monitor(
526 monitors: &WinitMonitors,
527 primary_monitor: Option<MonitorHandle>,
528 current_monitor: Option<MonitorHandle>,
529 monitor_selection: &MonitorSelection,
530) -> Option<MonitorHandle> {
531 use bevy_window::MonitorSelection::*;
532
533 match monitor_selection {
534 Current => {
535 if current_monitor.is_none() {
536 warn!("Can't select current monitor on window creation or cannot find current monitor!");
537 }
538 current_monitor
539 }
540 Primary => primary_monitor,
541 Index(n) => monitors.nth(*n),
542 Entity(entity) => monitors.find_entity(*entity),
543 }
544}
545
546struct DisplayInfo {
547 window_physical_resolution: (u32, u32),
548 window_logical_resolution: (f32, f32),
549 monitor_name: Option<String>,
550 scale_factor: Option<f64>,
551 refresh_rate_millihertz: Option<u32>,
552}
553
554impl core::fmt::Display for DisplayInfo {
555 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
556 write!(f, "Display information:")?;
557 write!(
558 f,
559 " Window physical resolution: {}x{}",
560 self.window_physical_resolution.0, self.window_physical_resolution.1
561 )?;
562 write!(
563 f,
564 " Window logical resolution: {}x{}",
565 self.window_logical_resolution.0, self.window_logical_resolution.1
566 )?;
567 write!(
568 f,
569 " Monitor name: {}",
570 self.monitor_name.as_deref().unwrap_or("")
571 )?;
572 write!(f, " Scale factor: {}", self.scale_factor.unwrap_or(0.))?;
573 let millihertz = self.refresh_rate_millihertz.unwrap_or(0);
574 let hertz = millihertz / 1000;
575 let extra_millihertz = millihertz % 1000;
576 write!(f, " Refresh rate (Hz): {hertz}.{extra_millihertz:03}")?;
577 Ok(())
578 }
579}