1use bevy_a11y::AccessibilityRequested;
2use bevy_ecs::entity::Entity;
3
4use bevy_ecs::entity::EntityHashMap;
5use bevy_utils::{tracing::warn, HashMap};
6use bevy_window::{
7 CursorGrabMode, MonitorSelection, Window, WindowMode, WindowPosition, WindowResolution,
8 WindowWrapper,
9};
10
11use winit::{
12 dpi::{LogicalSize, PhysicalPosition},
13 event_loop::ActiveEventLoop,
14 monitor::{MonitorHandle, VideoModeHandle},
15 window::{CursorGrabMode as WinitCursorGrabMode, Fullscreen, Window as WinitWindow, WindowId},
16};
17
18use crate::{
19 accessibility::{
20 prepare_accessibility_for_window, AccessKitAdapters, WinitActionRequestHandlers,
21 },
22 converters::{convert_enabled_buttons, convert_window_level, convert_window_theme},
23 winit_monitors::WinitMonitors,
24};
25
26#[derive(Debug, Default)]
29pub struct WinitWindows {
30 pub windows: HashMap<WindowId, WindowWrapper<WinitWindow>>,
32 pub entity_to_winit: EntityHashMap<WindowId>,
34 pub winit_to_entity: HashMap<WindowId, Entity>,
36 _not_send_sync: core::marker::PhantomData<*const ()>,
40}
41
42impl WinitWindows {
43 #[allow(clippy::too_many_arguments)]
45 pub fn create_window(
46 &mut self,
47 event_loop: &ActiveEventLoop,
48 entity: Entity,
49 window: &Window,
50 adapters: &mut AccessKitAdapters,
51 handlers: &mut WinitActionRequestHandlers,
52 accessibility_requested: &AccessibilityRequested,
53 monitors: &WinitMonitors,
54 ) -> &WindowWrapper<WinitWindow> {
55 let mut winit_window_attributes = WinitWindow::default_attributes();
56
57 winit_window_attributes = winit_window_attributes.with_visible(false);
60
61 let maybe_selected_monitor = &match window.mode {
62 WindowMode::BorderlessFullscreen(monitor_selection)
63 | WindowMode::Fullscreen(monitor_selection)
64 | WindowMode::SizedFullscreen(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(_) => {
77 let select_monitor = &maybe_selected_monitor
78 .clone()
79 .expect("Unable to get monitor.");
80 let videomode = get_best_videomode(select_monitor);
81 winit_window_attributes.with_fullscreen(Some(Fullscreen::Exclusive(videomode)))
82 }
83 WindowMode::SizedFullscreen(_) => {
84 let select_monitor = &maybe_selected_monitor
85 .clone()
86 .expect("Unable to get monitor.");
87 let videomode = get_fitting_videomode(
88 select_monitor,
89 window.width() as u32,
90 window.height() as u32,
91 );
92 winit_window_attributes.with_fullscreen(Some(Fullscreen::Exclusive(videomode)))
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
115 .with_window_level(convert_window_level(window.window_level))
116 .with_theme(window.window_theme.map(convert_window_theme))
117 .with_resizable(window.resizable)
118 .with_enabled_buttons(convert_enabled_buttons(window.enabled_buttons))
119 .with_decorations(window.decorations)
120 .with_transparent(window.transparent)
121 .with_visible(window.visible);
122
123 #[cfg(target_os = "windows")]
124 {
125 use winit::platform::windows::WindowAttributesExtWindows;
126 winit_window_attributes =
127 winit_window_attributes.with_skip_taskbar(window.skip_taskbar);
128 }
129
130 #[cfg(target_os = "macos")]
131 {
132 use winit::platform::macos::WindowAttributesExtMacOS;
133 winit_window_attributes = winit_window_attributes
134 .with_movable_by_window_background(window.movable_by_window_background)
135 .with_fullsize_content_view(window.fullsize_content_view)
136 .with_has_shadow(window.has_shadow)
137 .with_titlebar_hidden(!window.titlebar_shown)
138 .with_titlebar_transparent(window.titlebar_transparent)
139 .with_title_hidden(!window.titlebar_show_title)
140 .with_titlebar_buttons_hidden(!window.titlebar_show_buttons);
141 }
142
143 let display_info = DisplayInfo {
144 window_physical_resolution: (
145 window.resolution.physical_width(),
146 window.resolution.physical_height(),
147 ),
148 window_logical_resolution: (window.resolution.width(), window.resolution.height()),
149 monitor_name: maybe_selected_monitor
150 .as_ref()
151 .and_then(MonitorHandle::name),
152 scale_factor: maybe_selected_monitor
153 .as_ref()
154 .map(MonitorHandle::scale_factor),
155 refresh_rate_millihertz: maybe_selected_monitor
156 .as_ref()
157 .and_then(MonitorHandle::refresh_rate_millihertz),
158 };
159 bevy_log::debug!("{display_info}");
160
161 #[cfg(any(
162 target_os = "linux",
163 target_os = "dragonfly",
164 target_os = "freebsd",
165 target_os = "netbsd",
166 target_os = "openbsd",
167 target_os = "windows"
168 ))]
169 if let Some(name) = &window.name {
170 #[cfg(all(
171 feature = "wayland",
172 any(
173 target_os = "linux",
174 target_os = "dragonfly",
175 target_os = "freebsd",
176 target_os = "netbsd",
177 target_os = "openbsd"
178 )
179 ))]
180 {
181 winit_window_attributes =
182 winit::platform::wayland::WindowAttributesExtWayland::with_name(
183 winit_window_attributes,
184 name.clone(),
185 "",
186 );
187 }
188
189 #[cfg(all(
190 feature = "x11",
191 any(
192 target_os = "linux",
193 target_os = "dragonfly",
194 target_os = "freebsd",
195 target_os = "netbsd",
196 target_os = "openbsd"
197 )
198 ))]
199 {
200 winit_window_attributes = winit::platform::x11::WindowAttributesExtX11::with_name(
201 winit_window_attributes,
202 name.clone(),
203 "",
204 );
205 }
206 #[cfg(target_os = "windows")]
207 {
208 winit_window_attributes =
209 winit::platform::windows::WindowAttributesExtWindows::with_class_name(
210 winit_window_attributes,
211 name.clone(),
212 );
213 }
214 }
215
216 let constraints = window.resize_constraints.check_constraints();
217 let min_inner_size = LogicalSize {
218 width: constraints.min_width,
219 height: constraints.min_height,
220 };
221 let max_inner_size = LogicalSize {
222 width: constraints.max_width,
223 height: constraints.max_height,
224 };
225
226 let winit_window_attributes =
227 if constraints.max_width.is_finite() && constraints.max_height.is_finite() {
228 winit_window_attributes
229 .with_min_inner_size(min_inner_size)
230 .with_max_inner_size(max_inner_size)
231 } else {
232 winit_window_attributes.with_min_inner_size(min_inner_size)
233 };
234
235 #[allow(unused_mut)]
236 let mut winit_window_attributes = winit_window_attributes.with_title(window.title.as_str());
237
238 #[cfg(target_arch = "wasm32")]
239 {
240 use wasm_bindgen::JsCast;
241 use winit::platform::web::WindowAttributesExtWebSys;
242
243 if let Some(selector) = &window.canvas {
244 let window = web_sys::window().unwrap();
245 let document = window.document().unwrap();
246 let canvas = document
247 .query_selector(selector)
248 .expect("Cannot query for canvas element.");
249 if let Some(canvas) = canvas {
250 let canvas = canvas.dyn_into::<web_sys::HtmlCanvasElement>().ok();
251 winit_window_attributes = winit_window_attributes.with_canvas(canvas);
252 } else {
253 panic!("Cannot find element: {}.", selector);
254 }
255 }
256
257 winit_window_attributes =
258 winit_window_attributes.with_prevent_default(window.prevent_default_event_handling);
259 winit_window_attributes = winit_window_attributes.with_append(true);
260 }
261
262 let winit_window = event_loop.create_window(winit_window_attributes).unwrap();
263 let name = window.title.clone();
264 prepare_accessibility_for_window(
265 &winit_window,
266 entity,
267 name,
268 accessibility_requested.clone(),
269 adapters,
270 handlers,
271 );
272
273 if window.cursor_options.grab_mode != CursorGrabMode::None {
275 attempt_grab(&winit_window, window.cursor_options.grab_mode);
276 }
277
278 winit_window.set_cursor_visible(window.cursor_options.visible);
279
280 if !window.cursor_options.hit_test {
283 if let Err(err) = winit_window.set_cursor_hittest(window.cursor_options.hit_test) {
284 warn!(
285 "Could not set cursor hit test for window {:?}: {:?}",
286 window.title, err
287 );
288 }
289 }
290
291 self.entity_to_winit.insert(entity, winit_window.id());
292 self.winit_to_entity.insert(winit_window.id(), entity);
293
294 self.windows
295 .entry(winit_window.id())
296 .insert(WindowWrapper::new(winit_window))
297 .into_mut()
298 }
299
300 pub fn get_window(&self, entity: Entity) -> Option<&WindowWrapper<WinitWindow>> {
302 self.entity_to_winit
303 .get(&entity)
304 .and_then(|winit_id| self.windows.get(winit_id))
305 }
306
307 pub fn get_window_entity(&self, winit_id: WindowId) -> Option<Entity> {
311 self.winit_to_entity.get(&winit_id).cloned()
312 }
313
314 pub fn remove_window(&mut self, entity: Entity) -> Option<WindowWrapper<WinitWindow>> {
318 let winit_id = self.entity_to_winit.remove(&entity)?;
319 self.winit_to_entity.remove(&winit_id);
320 self.windows.remove(&winit_id)
321 }
322}
323
324pub fn get_fitting_videomode(monitor: &MonitorHandle, width: u32, height: u32) -> VideoModeHandle {
328 let mut modes = monitor.video_modes().collect::<Vec<_>>();
329
330 fn abs_diff(a: u32, b: u32) -> u32 {
331 if a > b {
332 return a - b;
333 }
334 b - a
335 }
336
337 modes.sort_by(|a, b| {
338 use core::cmp::Ordering::*;
339 match abs_diff(a.size().width, width).cmp(&abs_diff(b.size().width, width)) {
340 Equal => {
341 match abs_diff(a.size().height, height).cmp(&abs_diff(b.size().height, height)) {
342 Equal => b
343 .refresh_rate_millihertz()
344 .cmp(&a.refresh_rate_millihertz()),
345 default => default,
346 }
347 }
348 default => default,
349 }
350 });
351
352 modes.first().unwrap().clone()
353}
354
355pub fn get_best_videomode(monitor: &MonitorHandle) -> VideoModeHandle {
359 let mut modes = monitor.video_modes().collect::<Vec<_>>();
360 modes.sort_by(|a, b| {
361 use core::cmp::Ordering::*;
362 match b.size().width.cmp(&a.size().width) {
363 Equal => match b.size().height.cmp(&a.size().height) {
364 Equal => b
365 .refresh_rate_millihertz()
366 .cmp(&a.refresh_rate_millihertz()),
367 default => default,
368 },
369 default => default,
370 }
371 });
372
373 modes.first().unwrap().clone()
374}
375
376pub(crate) fn attempt_grab(winit_window: &WinitWindow, grab_mode: CursorGrabMode) {
377 let grab_result = match grab_mode {
378 CursorGrabMode::None => winit_window.set_cursor_grab(WinitCursorGrabMode::None),
379 CursorGrabMode::Confined => winit_window
380 .set_cursor_grab(WinitCursorGrabMode::Confined)
381 .or_else(|_e| winit_window.set_cursor_grab(WinitCursorGrabMode::Locked)),
382 CursorGrabMode::Locked => winit_window
383 .set_cursor_grab(WinitCursorGrabMode::Locked)
384 .or_else(|_e| winit_window.set_cursor_grab(WinitCursorGrabMode::Confined)),
385 };
386
387 if let Err(err) = grab_result {
388 let err_desc = match grab_mode {
389 CursorGrabMode::Confined | CursorGrabMode::Locked => "grab",
390 CursorGrabMode::None => "ungrab",
391 };
392
393 bevy_utils::tracing::error!("Unable to {} cursor: {}", err_desc, err);
394 }
395}
396
397pub fn winit_window_position(
401 position: &WindowPosition,
402 resolution: &WindowResolution,
403 monitors: &WinitMonitors,
404 primary_monitor: Option<MonitorHandle>,
405 current_monitor: Option<MonitorHandle>,
406) -> Option<PhysicalPosition<i32>> {
407 match position {
408 WindowPosition::Automatic => {
409 None
411 }
412 WindowPosition::Centered(monitor_selection) => {
413 let maybe_monitor = select_monitor(
414 monitors,
415 primary_monitor,
416 current_monitor,
417 monitor_selection,
418 );
419
420 if let Some(monitor) = maybe_monitor {
421 let screen_size = monitor.size();
422
423 let scale_factor = match resolution.scale_factor_override() {
424 Some(scale_factor_override) => scale_factor_override as f64,
425 None => monitor.scale_factor(),
428 };
429
430 let (width, height): (u32, u32) =
432 LogicalSize::new(resolution.width(), resolution.height())
433 .to_physical::<u32>(scale_factor)
434 .into();
435
436 let position = PhysicalPosition {
437 x: screen_size.width.saturating_sub(width) as f64 / 2.
438 + monitor.position().x as f64,
439 y: screen_size.height.saturating_sub(height) as f64 / 2.
440 + monitor.position().y as f64,
441 };
442
443 Some(position.cast::<i32>())
444 } else {
445 warn!("Couldn't get monitor selected with: {monitor_selection:?}");
446 None
447 }
448 }
449 WindowPosition::At(position) => {
450 Some(PhysicalPosition::new(position[0] as f64, position[1] as f64).cast::<i32>())
451 }
452 }
453}
454
455pub fn select_monitor(
457 monitors: &WinitMonitors,
458 primary_monitor: Option<MonitorHandle>,
459 current_monitor: Option<MonitorHandle>,
460 monitor_selection: &MonitorSelection,
461) -> Option<MonitorHandle> {
462 use bevy_window::MonitorSelection::*;
463
464 match monitor_selection {
465 Current => {
466 if current_monitor.is_none() {
467 warn!("Can't select current monitor on window creation or cannot find current monitor!");
468 }
469 current_monitor
470 }
471 Primary => primary_monitor,
472 Index(n) => monitors.nth(*n),
473 Entity(entity) => monitors.find_entity(*entity),
474 }
475}
476
477struct DisplayInfo {
478 window_physical_resolution: (u32, u32),
479 window_logical_resolution: (f32, f32),
480 monitor_name: Option<String>,
481 scale_factor: Option<f64>,
482 refresh_rate_millihertz: Option<u32>,
483}
484
485impl core::fmt::Display for DisplayInfo {
486 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
487 write!(f, "Display information:")?;
488 write!(
489 f,
490 " Window physical resolution: {}x{}",
491 self.window_physical_resolution.0, self.window_physical_resolution.1
492 )?;
493 write!(
494 f,
495 " Window logical resolution: {}x{}",
496 self.window_logical_resolution.0, self.window_logical_resolution.1
497 )?;
498 write!(
499 f,
500 " Monitor name: {}",
501 self.monitor_name.as_deref().unwrap_or("")
502 )?;
503 write!(f, " Scale factor: {}", self.scale_factor.unwrap_or(0.))?;
504 let millihertz = self.refresh_rate_millihertz.unwrap_or(0);
505 let hertz = millihertz / 1000;
506 let extra_millihertz = millihertz % 1000;
507 write!(f, " Refresh rate (Hz): {}.{:03}", hertz, extra_millihertz)?;
508 Ok(())
509 }
510}