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