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