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
400pub(crate) fn attempt_grab(
401 winit_window: &WinitWindow,
402 grab_mode: CursorGrabMode,
403) -> Result<(), ExternalError> {
404 let grab_result = match grab_mode {
405 CursorGrabMode::None => winit_window.set_cursor_grab(WinitCursorGrabMode::None),
406 CursorGrabMode::Confined => winit_window
407 .set_cursor_grab(WinitCursorGrabMode::Confined)
408 .or_else(|_e| winit_window.set_cursor_grab(WinitCursorGrabMode::Locked)),
409 CursorGrabMode::Locked => winit_window
410 .set_cursor_grab(WinitCursorGrabMode::Locked)
411 .or_else(|_e| winit_window.set_cursor_grab(WinitCursorGrabMode::Confined)),
412 };
413
414 if let Err(err) = grab_result {
415 let err_desc = match grab_mode {
416 CursorGrabMode::Confined | CursorGrabMode::Locked => "grab",
417 CursorGrabMode::None => "ungrab",
418 };
419
420 tracing::error!("Unable to {} cursor: {}", err_desc, err);
421 Err(err)
422 } else {
423 Ok(())
424 }
425}
426
427pub fn winit_window_position(
431 position: &WindowPosition,
432 resolution: &WindowResolution,
433 monitors: &WinitMonitors,
434 primary_monitor: Option<MonitorHandle>,
435 current_monitor: Option<MonitorHandle>,
436) -> Option<PhysicalPosition<i32>> {
437 match position {
438 WindowPosition::Automatic => {
439 None
441 }
442 WindowPosition::Centered(monitor_selection) => {
443 let maybe_monitor = select_monitor(
444 monitors,
445 primary_monitor,
446 current_monitor,
447 monitor_selection,
448 );
449
450 if let Some(monitor) = maybe_monitor {
451 let screen_size = monitor.size();
452
453 let scale_factor = match resolution.scale_factor_override() {
454 Some(scale_factor_override) => scale_factor_override as f64,
455 None => monitor.scale_factor(),
458 };
459
460 let (width, height): (u32, u32) =
462 LogicalSize::new(resolution.width(), resolution.height())
463 .to_physical::<u32>(scale_factor)
464 .into();
465
466 let position = PhysicalPosition {
467 x: screen_size.width.saturating_sub(width) as f64 / 2.
468 + monitor.position().x as f64,
469 y: screen_size.height.saturating_sub(height) as f64 / 2.
470 + monitor.position().y as f64,
471 };
472
473 Some(position.cast::<i32>())
474 } else {
475 warn!("Couldn't get monitor selected with: {monitor_selection:?}");
476 None
477 }
478 }
479 WindowPosition::At(position) => {
480 Some(PhysicalPosition::new(position[0] as f64, position[1] as f64).cast::<i32>())
481 }
482 }
483}
484
485pub fn select_monitor(
487 monitors: &WinitMonitors,
488 primary_monitor: Option<MonitorHandle>,
489 current_monitor: Option<MonitorHandle>,
490 monitor_selection: &MonitorSelection,
491) -> Option<MonitorHandle> {
492 use bevy_window::MonitorSelection::*;
493
494 match monitor_selection {
495 Current => {
496 if current_monitor.is_none() {
497 warn!("Can't select current monitor on window creation or cannot find current monitor!");
498 }
499 current_monitor
500 }
501 Primary => primary_monitor,
502 Index(n) => monitors.nth(*n),
503 Entity(entity) => monitors.find_entity(*entity),
504 }
505}
506
507struct DisplayInfo {
508 window_physical_resolution: (u32, u32),
509 window_logical_resolution: (f32, f32),
510 monitor_name: Option<String>,
511 scale_factor: Option<f64>,
512 refresh_rate_millihertz: Option<u32>,
513}
514
515impl core::fmt::Display for DisplayInfo {
516 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
517 write!(f, "Display information:")?;
518 write!(
519 f,
520 " Window physical resolution: {}x{}",
521 self.window_physical_resolution.0, self.window_physical_resolution.1
522 )?;
523 write!(
524 f,
525 " Window logical resolution: {}x{}",
526 self.window_logical_resolution.0, self.window_logical_resolution.1
527 )?;
528 write!(
529 f,
530 " Monitor name: {}",
531 self.monitor_name.as_deref().unwrap_or("")
532 )?;
533 write!(f, " Scale factor: {}", self.scale_factor.unwrap_or(0.))?;
534 let millihertz = self.refresh_rate_millihertz.unwrap_or(0);
535 let hertz = millihertz / 1000;
536 let extra_millihertz = millihertz % 1000;
537 write!(f, " Refresh rate (Hz): {hertz}.{extra_millihertz:03}")?;
538 Ok(())
539 }
540}