Skip to main content

bevy_winit/
winit_windows.rs

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/// A resource mapping window entities to their `winit`-backend [`Window`](winit::window::Window)
29/// states.
30#[derive(Debug, Default)]
31pub struct WinitWindows {
32    /// Stores [`winit`] windows by window identifier.
33    pub windows: HashMap<WindowId, WindowWrapper<WinitWindow>>,
34    /// Maps entities to `winit` window identifiers.
35    pub entity_to_winit: EntityHashMap<WindowId>,
36    /// Maps `winit` window identifiers to entities.
37    pub winit_to_entity: HashMap<WindowId, Entity>,
38    // Many `winit` window functions (e.g. `set_window_icon`) can only be called on the main thread.
39    // If they're called on other threads, the program might hang. This marker indicates that this
40    // type is not thread-safe and will be `!Send` and `!Sync`.
41    _not_send_sync: core::marker::PhantomData<*const ()>,
42}
43
44impl WinitWindows {
45    /// Creates a new instance of `WinitWindows`.
46    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    /// Creates a `winit` window and associates it with our entity.
56    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        // Due to a UIA limitation, winit windows need to be invisible for the
70        // AccessKit adapter is initialized.
71        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                winit_window_attributes.with_fullscreen(Some(resolve_exclusive_fullscreen(
89                    maybe_selected_monitor.clone(),
90                    monitor_selection,
91                    video_mode_selection,
92                )))
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        // It's crucial to avoid setting the window's final visibility here;
115        // as explained above, the window must be invisible until the AccessKit
116        // adapter is created.
117        winit_window_attributes = winit_window_attributes
118            .with_window_level(convert_window_level(window.window_level))
119            .with_theme(window.window_theme.map(convert_window_theme))
120            .with_resizable(window.resizable)
121            .with_enabled_buttons(convert_enabled_buttons(window.enabled_buttons))
122            .with_decorations(window.decorations)
123            .with_transparent(window.transparent)
124            .with_active(window.focused);
125
126        #[cfg(target_os = "windows")]
127        {
128            use winit::platform::windows::WindowAttributesExtWindows;
129            winit_window_attributes =
130                winit_window_attributes.with_skip_taskbar(window.skip_taskbar);
131            winit_window_attributes =
132                winit_window_attributes.with_clip_children(window.clip_children);
133        }
134
135        #[cfg(target_os = "macos")]
136        {
137            use winit::platform::macos::WindowAttributesExtMacOS;
138            winit_window_attributes = winit_window_attributes
139                .with_movable_by_window_background(window.movable_by_window_background)
140                .with_fullsize_content_view(window.fullsize_content_view)
141                .with_has_shadow(window.has_shadow)
142                .with_titlebar_hidden(!window.titlebar_shown)
143                .with_titlebar_transparent(window.titlebar_transparent)
144                .with_title_hidden(!window.titlebar_show_title)
145                .with_titlebar_buttons_hidden(!window.titlebar_show_buttons)
146                .with_borderless_game(window.borderless_game);
147        }
148
149        #[cfg(target_os = "ios")]
150        {
151            use crate::converters::convert_screen_edge;
152            use winit::platform::ios::WindowAttributesExtIOS;
153
154            let preferred_edge =
155                convert_screen_edge(window.preferred_screen_edges_deferring_system_gestures);
156
157            winit_window_attributes = winit_window_attributes
158                .with_preferred_screen_edges_deferring_system_gestures(preferred_edge);
159            winit_window_attributes = winit_window_attributes
160                .with_prefers_home_indicator_hidden(window.prefers_home_indicator_hidden);
161            winit_window_attributes = winit_window_attributes
162                .with_prefers_status_bar_hidden(window.prefers_status_bar_hidden);
163        }
164
165        let display_info = DisplayInfo {
166            window_physical_resolution: (
167                window.resolution.physical_width(),
168                window.resolution.physical_height(),
169            ),
170            window_logical_resolution: (window.resolution.width(), window.resolution.height()),
171            monitor_name: maybe_selected_monitor
172                .as_ref()
173                .and_then(MonitorHandle::name),
174            scale_factor: maybe_selected_monitor
175                .as_ref()
176                .map(MonitorHandle::scale_factor),
177            refresh_rate_millihertz: maybe_selected_monitor
178                .as_ref()
179                .and_then(MonitorHandle::refresh_rate_millihertz),
180        };
181        bevy_log::debug!("{display_info}");
182
183        #[cfg(any(
184            all(
185                any(feature = "wayland", feature = "x11"),
186                any(
187                    target_os = "linux",
188                    target_os = "dragonfly",
189                    target_os = "freebsd",
190                    target_os = "netbsd",
191                    target_os = "openbsd",
192                )
193            ),
194            target_os = "windows"
195        ))]
196        if let Some(name) = &window.name {
197            #[cfg(all(
198                feature = "wayland",
199                any(
200                    target_os = "linux",
201                    target_os = "dragonfly",
202                    target_os = "freebsd",
203                    target_os = "netbsd",
204                    target_os = "openbsd"
205                )
206            ))]
207            {
208                winit_window_attributes =
209                    winit::platform::wayland::WindowAttributesExtWayland::with_name(
210                        winit_window_attributes,
211                        name.clone(),
212                        "",
213                    );
214            }
215
216            #[cfg(all(
217                feature = "x11",
218                any(
219                    target_os = "linux",
220                    target_os = "dragonfly",
221                    target_os = "freebsd",
222                    target_os = "netbsd",
223                    target_os = "openbsd"
224                )
225            ))]
226            {
227                winit_window_attributes = winit::platform::x11::WindowAttributesExtX11::with_name(
228                    winit_window_attributes,
229                    name.clone(),
230                    "",
231                );
232            }
233            #[cfg(target_os = "windows")]
234            {
235                winit_window_attributes =
236                    winit::platform::windows::WindowAttributesExtWindows::with_class_name(
237                        winit_window_attributes,
238                        name.clone(),
239                    );
240            }
241        }
242
243        let constraints = window.resize_constraints.check_constraints();
244        let min_inner_size = LogicalSize {
245            width: constraints.min_width,
246            height: constraints.min_height,
247        };
248        let max_inner_size = LogicalSize {
249            width: constraints.max_width,
250            height: constraints.max_height,
251        };
252
253        let winit_window_attributes =
254            if constraints.max_width.is_finite() && constraints.max_height.is_finite() {
255                winit_window_attributes
256                    .with_min_inner_size(min_inner_size)
257                    .with_max_inner_size(max_inner_size)
258            } else {
259                winit_window_attributes.with_min_inner_size(min_inner_size)
260            };
261
262        #[expect(clippy::allow_attributes, reason = "`unused_mut` is not always linted")]
263        #[allow(
264            unused_mut,
265            reason = "This variable needs to be mutable if `cfg(target_arch = \"wasm32\")`"
266        )]
267        let mut winit_window_attributes = winit_window_attributes.with_title(window.title.as_str());
268
269        #[cfg(target_arch = "wasm32")]
270        {
271            use wasm_bindgen::JsCast;
272            use winit::platform::web::WindowAttributesExtWebSys;
273
274            if let Some(selector) = &window.canvas {
275                let window = web_sys::window().unwrap();
276                let document = window.document().unwrap();
277                let canvas = document
278                    .query_selector(selector)
279                    .expect("Cannot query for canvas element.");
280                if let Some(canvas) = canvas {
281                    let canvas = canvas.dyn_into::<web_sys::HtmlCanvasElement>().ok();
282                    winit_window_attributes = winit_window_attributes.with_canvas(canvas);
283                } else {
284                    panic!("Cannot find element: {selector}.");
285                }
286            }
287
288            winit_window_attributes =
289                winit_window_attributes.with_prevent_default(window.prevent_default_event_handling);
290            winit_window_attributes = winit_window_attributes.with_append(true);
291        }
292
293        let winit_window = event_loop.create_window(winit_window_attributes).unwrap();
294        let name = window.title.clone();
295        prepare_accessibility_for_window(
296            event_loop,
297            &winit_window,
298            entity,
299            name,
300            accessibility_requested.clone(),
301            adapters,
302            handlers,
303        );
304
305        // Now that the AccessKit adapter is created, it's safe to show
306        // the window.
307        winit_window.set_visible(window.visible);
308
309        // Do not set the grab mode on window creation if it's none. It can fail on mobile.
310        if cursor_options.grab_mode != CursorGrabMode::None {
311            let _ = attempt_grab(&winit_window, cursor_options.grab_mode);
312        }
313
314        winit_window.set_cursor_visible(cursor_options.visible);
315
316        // Do not set the cursor hittest on window creation if it's false, as it will always fail on
317        // some platforms and log an unfixable warning.
318        if !cursor_options.hit_test
319            && let Err(err) = winit_window.set_cursor_hittest(cursor_options.hit_test)
320        {
321            warn!(
322                "Could not set cursor hit test for window {}: {}",
323                window.title, err
324            );
325        }
326
327        self.entity_to_winit.insert(entity, winit_window.id());
328        self.winit_to_entity.insert(winit_window.id(), entity);
329
330        self.windows
331            .entry(winit_window.id())
332            .insert(WindowWrapper::new(winit_window))
333            .into_mut()
334    }
335
336    /// Get the winit window that is associated with our entity.
337    pub fn get_window(&self, entity: Entity) -> Option<&WindowWrapper<WinitWindow>> {
338        self.entity_to_winit
339            .get(&entity)
340            .and_then(|winit_id| self.windows.get(winit_id))
341    }
342
343    /// Get the entity associated with the winit window id.
344    ///
345    /// This is mostly just an intermediary step between us and winit.
346    pub fn get_window_entity(&self, winit_id: WindowId) -> Option<Entity> {
347        self.winit_to_entity.get(&winit_id).cloned()
348    }
349
350    /// Remove a window from winit.
351    ///
352    /// This should mostly just be called when the window is closing.
353    pub fn remove_window(&mut self, entity: Entity) -> Option<WindowWrapper<WinitWindow>> {
354        let winit_id = self.entity_to_winit.remove(&entity)?;
355        self.winit_to_entity.remove(&winit_id);
356        self.windows.remove(&winit_id)
357    }
358}
359
360/// Returns some [`winit::monitor::VideoModeHandle`] given a [`MonitorHandle`] and a
361/// [`VideoModeSelection`] or None if no valid matching video mode was found.
362pub fn get_selected_videomode(
363    monitor: &MonitorHandle,
364    selection: &VideoModeSelection,
365) -> Option<VideoModeHandle> {
366    match selection {
367        VideoModeSelection::Current => get_current_videomode(monitor),
368        VideoModeSelection::Specific(specified) => monitor.video_modes().find(|mode| {
369            mode.size().width == specified.physical_size.x
370                && mode.size().height == specified.physical_size.y
371                && mode.refresh_rate_millihertz() == specified.refresh_rate_millihertz
372                && mode.bit_depth() == specified.bit_depth
373        }),
374    }
375}
376
377/// Resolves a `WindowMode::Fullscreen` request to a [`Fullscreen`] value.
378///
379/// Tries exclusive fullscreen first; falls back to borderless fullscreen and logs a
380/// warning if the monitor cannot be resolved or no matching video mode is available.
381pub(crate) fn resolve_exclusive_fullscreen(
382    monitor: Option<MonitorHandle>,
383    monitor_selection: MonitorSelection,
384    video_mode_selection: VideoModeSelection,
385) -> Fullscreen {
386    let video_mode = monitor
387        .as_ref()
388        .and_then(|m| get_selected_videomode(m, &video_mode_selection));
389    if let Some(video_mode) = video_mode {
390        return Fullscreen::Exclusive(video_mode);
391    }
392    if monitor.is_none() {
393        warn!(
394            "Could not find monitor for {:?}; falling back to borderless fullscreen",
395            monitor_selection
396        );
397    } else {
398        warn!(
399            "Could not find valid fullscreen video mode for {:?} {:?}; falling back to borderless fullscreen",
400            monitor_selection, video_mode_selection
401        );
402    }
403    Fullscreen::Borderless(monitor)
404}
405
406/// Gets a monitor's current video-mode.
407///
408// TODO: When Winit 0.31 releases this function can be removed and replaced with
409// `MonitorHandle::current_video_mode()`
410fn get_current_videomode(monitor: &MonitorHandle) -> Option<VideoModeHandle> {
411    monitor
412        .video_modes()
413        .filter(|mode| {
414            mode.size() == monitor.size()
415                && Some(mode.refresh_rate_millihertz()) == monitor.refresh_rate_millihertz()
416        })
417        .max_by_key(VideoModeHandle::bit_depth)
418}
419
420#[cfg(target_arch = "wasm32")]
421fn pointer_supported() -> Result<bool, ExternalError> {
422    Ok(js_sys::Reflect::has(
423        web_sys::window()
424            .ok_or(ExternalError::Ignored)?
425            .document()
426            .ok_or(ExternalError::Ignored)?
427            .as_ref(),
428        &"exitPointerLock".into(),
429    )
430    .unwrap_or(false))
431}
432
433pub(crate) fn attempt_grab(
434    winit_window: &WinitWindow,
435    grab_mode: CursorGrabMode,
436) -> Result<(), ExternalError> {
437    // Do not attempt to grab on web if unsupported (e.g. mobile)
438    #[cfg(target_arch = "wasm32")]
439    if !pointer_supported()? {
440        return Err(ExternalError::Ignored);
441    }
442
443    let grab_result = match grab_mode {
444        CursorGrabMode::None => winit_window.set_cursor_grab(WinitCursorGrabMode::None),
445        CursorGrabMode::Confined => winit_window
446            .set_cursor_grab(WinitCursorGrabMode::Confined)
447            .or_else(|_e| winit_window.set_cursor_grab(WinitCursorGrabMode::None)),
448        CursorGrabMode::Locked => winit_window
449            .set_cursor_grab(WinitCursorGrabMode::Locked)
450            .or_else(|_e| winit_window.set_cursor_grab(WinitCursorGrabMode::Confined)),
451    };
452
453    if let Err(err) = grab_result {
454        let err_desc = match grab_mode {
455            CursorGrabMode::Confined | CursorGrabMode::Locked => "grab",
456            CursorGrabMode::None => "ungrab",
457        };
458
459        tracing::error!("Unable to {} cursor: {}", err_desc, err);
460        Err(err)
461    } else {
462        Ok(())
463    }
464}
465
466/// Compute the physical window position for a given [`WindowPosition`].
467// Ideally we could generify this across window backends, but we only really have winit atm
468// so whatever.
469pub fn winit_window_position(
470    position: &WindowPosition,
471    resolution: &WindowResolution,
472    monitors: &WinitMonitors,
473    primary_monitor: Option<MonitorHandle>,
474    current_monitor: Option<MonitorHandle>,
475) -> Option<PhysicalPosition<i32>> {
476    match position {
477        WindowPosition::Automatic => {
478            // Window manager will handle position
479            None
480        }
481        WindowPosition::Centered(monitor_selection) => {
482            let maybe_monitor = select_monitor(
483                monitors,
484                primary_monitor,
485                current_monitor,
486                monitor_selection,
487            );
488
489            if let Some(monitor) = maybe_monitor {
490                let screen_size = monitor.size();
491
492                let scale_factor = match resolution.scale_factor_override() {
493                    Some(scale_factor_override) => scale_factor_override as f64,
494                    // We use the monitors scale factor here since `WindowResolution.scale_factor` is
495                    // not yet populated when windows are created during plugin setup.
496                    None => monitor.scale_factor(),
497                };
498
499                // Logical to physical window size
500                let (width, height): (u32, u32) =
501                    LogicalSize::new(resolution.width(), resolution.height())
502                        .to_physical::<u32>(scale_factor)
503                        .into();
504
505                let position = PhysicalPosition {
506                    x: screen_size.width.saturating_sub(width) as f64 / 2.
507                        + monitor.position().x as f64,
508                    y: screen_size.height.saturating_sub(height) as f64 / 2.
509                        + monitor.position().y as f64,
510                };
511
512                Some(position.cast::<i32>())
513            } else {
514                warn!("Couldn't get monitor selected with: {monitor_selection:?}");
515                None
516            }
517        }
518        WindowPosition::At(position) => {
519            Some(PhysicalPosition::new(position[0] as f64, position[1] as f64).cast::<i32>())
520        }
521    }
522}
523
524/// Selects a monitor based on the given [`MonitorSelection`].
525pub fn select_monitor(
526    monitors: &WinitMonitors,
527    primary_monitor: Option<MonitorHandle>,
528    current_monitor: Option<MonitorHandle>,
529    monitor_selection: &MonitorSelection,
530) -> Option<MonitorHandle> {
531    use bevy_window::MonitorSelection::*;
532
533    match monitor_selection {
534        Current => {
535            if current_monitor.is_none() {
536                warn!("Can't select current monitor on window creation or cannot find current monitor!");
537            }
538            current_monitor
539        }
540        Primary => primary_monitor,
541        Index(n) => monitors.nth(*n),
542        Entity(entity) => monitors.find_entity(*entity),
543    }
544}
545
546struct DisplayInfo {
547    window_physical_resolution: (u32, u32),
548    window_logical_resolution: (f32, f32),
549    monitor_name: Option<String>,
550    scale_factor: Option<f64>,
551    refresh_rate_millihertz: Option<u32>,
552}
553
554impl core::fmt::Display for DisplayInfo {
555    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
556        write!(f, "Display information:")?;
557        write!(
558            f,
559            "  Window physical resolution: {}x{}",
560            self.window_physical_resolution.0, self.window_physical_resolution.1
561        )?;
562        write!(
563            f,
564            "  Window logical resolution: {}x{}",
565            self.window_logical_resolution.0, self.window_logical_resolution.1
566        )?;
567        write!(
568            f,
569            "  Monitor name: {}",
570            self.monitor_name.as_deref().unwrap_or("")
571        )?;
572        write!(f, "  Scale factor: {}", self.scale_factor.unwrap_or(0.))?;
573        let millihertz = self.refresh_rate_millihertz.unwrap_or(0);
574        let hertz = millihertz / 1000;
575        let extra_millihertz = millihertz % 1000;
576        write!(f, "  Refresh rate (Hz): {hertz}.{extra_millihertz:03}")?;
577        Ok(())
578    }
579}