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