bevy_winit/
winit_windows.rs

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/// A resource mapping window entities to their `winit`-backend [`Window`](winit::window::Window)
27/// states.
28#[derive(Debug, Default)]
29pub struct WinitWindows {
30    /// Stores [`winit`] windows by window identifier.
31    pub windows: HashMap<WindowId, WindowWrapper<WinitWindow>>,
32    /// Maps entities to `winit` window identifiers.
33    pub entity_to_winit: EntityHashMap<WindowId>,
34    /// Maps `winit` window identifiers to entities.
35    pub winit_to_entity: HashMap<WindowId, Entity>,
36    // Many `winit` window functions (e.g. `set_window_icon`) can only be called on the main thread.
37    // If they're called on other threads, the program might hang. This marker indicates that this
38    // type is not thread-safe and will be `!Send` and `!Sync`.
39    _not_send_sync: core::marker::PhantomData<*const ()>,
40}
41
42impl WinitWindows {
43    /// Creates a `winit` window and associates it with our entity.
44    #[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        // Due to a UIA limitation, winit windows need to be invisible for the
58        // AccessKit adapter is initialized.
59        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        // Do not set the grab mode on window creation if it's none. It can fail on mobile.
274        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        // Do not set the cursor hittest on window creation if it's false, as it will always fail on
281        // some platforms and log an unfixable warning.
282        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    /// Get the winit window that is associated with our entity.
301    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    /// Get the entity associated with the winit window id.
308    ///
309    /// This is mostly just an intermediary step between us and winit.
310    pub fn get_window_entity(&self, winit_id: WindowId) -> Option<Entity> {
311        self.winit_to_entity.get(&winit_id).cloned()
312    }
313
314    /// Remove a window from winit.
315    ///
316    /// This should mostly just be called when the window is closing.
317    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
324/// Gets the "best" video mode which fits the given dimensions.
325///
326/// The heuristic for "best" prioritizes width, height, and refresh rate in that order.
327pub 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
355/// Gets the "best" video-mode handle from a monitor.
356///
357/// The heuristic for "best" prioritizes width, height, and refresh rate in that order.
358pub 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
397/// Compute the physical window position for a given [`WindowPosition`].
398// Ideally we could generify this across window backends, but we only really have winit atm
399// so whatever.
400pub 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            // Window manager will handle position
410            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                    // We use the monitors scale factor here since `WindowResolution.scale_factor` is
426                    // not yet populated when windows are created during plugin setup.
427                    None => monitor.scale_factor(),
428                };
429
430                // Logical to physical window size
431                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
455/// Selects a monitor based on the given [`MonitorSelection`].
456pub 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}