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