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, 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/// 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 `winit` window and associates it with our entity.
46    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        // Due to a UIA limitation, winit windows need to be invisible for the
59        // AccessKit adapter is initialized.
60        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        // It's crucial to avoid setting the window's final visibility here;
114        // as explained above, the window must be invisible until the AccessKit
115        // adapter is created.
116        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        // Now that the AccessKit adapter is created, it's safe to show
291        // the window.
292        winit_window.set_visible(window.visible);
293
294        // Do not set the grab mode on window creation if it's none. It can fail on mobile.
295        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        // Do not set the cursor hittest on window creation if it's false, as it will always fail on
302        // some platforms and log an unfixable warning.
303        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    /// Get the winit window that is associated with our entity.
322    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    /// Get the entity associated with the winit window id.
329    ///
330    /// This is mostly just an intermediary step between us and winit.
331    pub fn get_window_entity(&self, winit_id: WindowId) -> Option<Entity> {
332        self.winit_to_entity.get(&winit_id).cloned()
333    }
334
335    /// Remove a window from winit.
336    ///
337    /// This should mostly just be called when the window is closing.
338    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
345/// Returns some [`winit::monitor::VideoModeHandle`] given a [`MonitorHandle`] and a
346/// [`VideoModeSelection`] or None if no valid matching video mode was found.
347pub 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
362/// Gets a monitor's current video-mode.
363///
364/// TODO: When Winit 0.31 releases this function can be removed and replaced with
365/// `MonitorHandle::current_video_mode()`
366fn 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
403/// Compute the physical window position for a given [`WindowPosition`].
404// Ideally we could generify this across window backends, but we only really have winit atm
405// so whatever.
406pub 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            // Window manager will handle position
416            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                    // We use the monitors scale factor here since `WindowResolution.scale_factor` is
432                    // not yet populated when windows are created during plugin setup.
433                    None => monitor.scale_factor(),
434                };
435
436                // Logical to physical window size
437                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
461/// Selects a monitor based on the given [`MonitorSelection`].
462pub 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}