bevy_winit/
system.rs

1use std::collections::HashMap;
2
3use bevy_derive::{Deref, DerefMut};
4use bevy_ecs::{
5    change_detection::DetectChangesMut,
6    entity::Entity,
7    lifecycle::RemovedComponents,
8    message::MessageWriter,
9    prelude::{Changed, Component},
10    system::{Local, NonSendMarker, Query, SystemParamItem},
11};
12use bevy_input::keyboard::{Key, KeyCode, KeyboardFocusLost, KeyboardInput};
13use bevy_window::{
14    ClosingWindow, CursorOptions, Monitor, PrimaryMonitor, RawHandleWrapper, VideoMode, Window,
15    WindowClosed, WindowClosing, WindowCreated, WindowEvent, WindowFocused, WindowMode,
16    WindowResized, WindowWrapper,
17};
18use tracing::{error, info, warn};
19
20use winit::{
21    dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize},
22    event_loop::ActiveEventLoop,
23};
24
25use bevy_app::AppExit;
26use bevy_ecs::{prelude::MessageReader, query::With, system::Res};
27use bevy_math::{IVec2, UVec2};
28#[cfg(target_os = "ios")]
29use winit::platform::ios::WindowExtIOS;
30#[cfg(target_arch = "wasm32")]
31use winit::platform::web::WindowExtWebSys;
32
33use crate::{
34    accessibility::ACCESS_KIT_ADAPTERS,
35    converters::{
36        convert_enabled_buttons, convert_resize_direction, convert_window_level,
37        convert_window_theme, convert_winit_theme,
38    },
39    get_selected_videomode, select_monitor,
40    state::react_to_resize,
41    winit_monitors::WinitMonitors,
42    CreateMonitorParams, CreateWindowParams, WINIT_WINDOWS,
43};
44
45/// Creates new windows on the [`winit`] backend for each entity with a newly-added
46/// [`Window`] component.
47///
48/// If any of these entities are missing required components, those will be added with their
49/// default values.
50pub fn create_windows(
51    event_loop: &ActiveEventLoop,
52    (
53        mut commands,
54        mut created_windows,
55        mut window_created_events,
56        mut handlers,
57        accessibility_requested,
58        monitors,
59    ): SystemParamItem<CreateWindowParams>,
60) {
61    WINIT_WINDOWS.with_borrow_mut(|winit_windows| {
62        ACCESS_KIT_ADAPTERS.with_borrow_mut(|adapters| {
63            for (entity, mut window, cursor_options, handle_holder) in &mut created_windows {
64                if winit_windows.get_window(entity).is_some() {
65                    continue;
66                }
67
68                info!("Creating new window {} ({})", window.title.as_str(), entity);
69
70                let winit_window = winit_windows.create_window(
71                    event_loop,
72                    entity,
73                    &window,
74                    cursor_options,
75                    adapters,
76                    &mut handlers,
77                    &accessibility_requested,
78                    &monitors,
79                );
80
81                if let Some(theme) = winit_window.theme() {
82                    window.window_theme = Some(convert_winit_theme(theme));
83                }
84
85                window
86                    .resolution
87                    .set_scale_factor_and_apply_to_physical_size(winit_window.scale_factor() as f32);
88
89                commands.entity(entity).insert((
90                    CachedWindow(window.clone()),
91                    CachedCursorOptions(cursor_options.clone()),
92                    WinitWindowPressedKeys::default(),
93                ));
94
95                if let Ok(handle_wrapper) = RawHandleWrapper::new(winit_window) {
96                    commands.entity(entity).insert(handle_wrapper.clone());
97                    if let Some(handle_holder) = handle_holder {
98                        *handle_holder.0.lock().unwrap() = Some(handle_wrapper);
99                    }
100                }
101
102                #[cfg(target_arch = "wasm32")]
103                {
104                    if window.fit_canvas_to_parent {
105                        let canvas = winit_window
106                            .canvas()
107                            .expect("window.canvas() can only be called in main thread.");
108                        let style = canvas.style();
109                        style.set_property("width", "100%").unwrap();
110                        style.set_property("height", "100%").unwrap();
111                    }
112                }
113
114                #[cfg(target_os = "ios")]
115                {
116                    winit_window.recognize_pinch_gesture(window.recognize_pinch_gesture);
117                    winit_window.recognize_rotation_gesture(window.recognize_rotation_gesture);
118                    winit_window.recognize_doubletap_gesture(window.recognize_doubletap_gesture);
119                    if let Some((min, max)) = window.recognize_pan_gesture {
120                        winit_window.recognize_pan_gesture(true, min, max);
121                    } else {
122                        winit_window.recognize_pan_gesture(false, 0, 0);
123                    }
124                }
125
126                window_created_events.write(WindowCreated { window: entity });
127            }
128        });
129    });
130}
131
132/// Check whether keyboard focus was lost. This is different from window
133/// focus in that swapping between Bevy windows keeps window focus.
134pub(crate) fn check_keyboard_focus_lost(
135    mut window_focused_reader: MessageReader<WindowFocused>,
136    mut keyboard_focus_lost_writer: MessageWriter<KeyboardFocusLost>,
137    mut keyboard_input_writer: MessageWriter<KeyboardInput>,
138    mut window_event_writer: MessageWriter<WindowEvent>,
139    mut q_windows: Query<&mut WinitWindowPressedKeys>,
140) {
141    let mut focus_lost = vec![];
142    let mut focus_gained = false;
143    for e in window_focused_reader.read() {
144        if e.focused {
145            focus_gained = true;
146        } else {
147            focus_lost.push(e.window);
148        }
149    }
150
151    if !focus_gained {
152        if !focus_lost.is_empty() {
153            window_event_writer.write(WindowEvent::KeyboardFocusLost(KeyboardFocusLost));
154            keyboard_focus_lost_writer.write(KeyboardFocusLost);
155        }
156
157        for window in focus_lost {
158            let Ok(mut pressed_keys) = q_windows.get_mut(window) else {
159                continue;
160            };
161            for (key_code, logical_key) in pressed_keys.0.drain() {
162                let event = KeyboardInput {
163                    key_code,
164                    logical_key,
165                    state: bevy_input::ButtonState::Released,
166                    repeat: false,
167                    window,
168                    text: None,
169                };
170                window_event_writer.write(WindowEvent::KeyboardInput(event.clone()));
171                keyboard_input_writer.write(event);
172            }
173        }
174    }
175}
176
177/// Synchronize available monitors as reported by [`winit`] with [`Monitor`] entities in the world.
178pub fn create_monitors(
179    event_loop: &ActiveEventLoop,
180    (mut commands, mut monitors): SystemParamItem<CreateMonitorParams>,
181) {
182    let primary_monitor = event_loop.primary_monitor();
183    let mut seen_monitors = vec![false; monitors.monitors.len()];
184
185    'outer: for monitor in event_loop.available_monitors() {
186        for (idx, (m, _)) in monitors.monitors.iter().enumerate() {
187            if &monitor == m {
188                seen_monitors[idx] = true;
189                continue 'outer;
190            }
191        }
192
193        let size = monitor.size();
194        let position = monitor.position();
195
196        let entity = commands
197            .spawn(Monitor {
198                name: monitor.name(),
199                physical_height: size.height,
200                physical_width: size.width,
201                physical_position: IVec2::new(position.x, position.y),
202                refresh_rate_millihertz: monitor.refresh_rate_millihertz(),
203                scale_factor: monitor.scale_factor(),
204                video_modes: monitor
205                    .video_modes()
206                    .map(|v| {
207                        let size = v.size();
208                        VideoMode {
209                            physical_size: UVec2::new(size.width, size.height),
210                            bit_depth: v.bit_depth(),
211                            refresh_rate_millihertz: v.refresh_rate_millihertz(),
212                        }
213                    })
214                    .collect(),
215            })
216            .id();
217
218        if primary_monitor.as_ref() == Some(&monitor) {
219            commands.entity(entity).insert(PrimaryMonitor);
220        }
221
222        seen_monitors.push(true);
223        monitors.monitors.push((monitor, entity));
224    }
225
226    let mut idx = 0;
227    monitors.monitors.retain(|(_m, entity)| {
228        if seen_monitors[idx] {
229            idx += 1;
230            true
231        } else {
232            info!("Monitor removed {}", entity);
233            commands.entity(*entity).despawn();
234            idx += 1;
235            false
236        }
237    });
238}
239
240pub(crate) fn despawn_windows(
241    closing: Query<Entity, With<ClosingWindow>>,
242    mut closed: RemovedComponents<Window>,
243    window_entities: Query<Entity, With<Window>>,
244    mut closing_event_writer: MessageWriter<WindowClosing>,
245    mut closed_event_writer: MessageWriter<WindowClosed>,
246    mut windows_to_drop: Local<Vec<WindowWrapper<winit::window::Window>>>,
247    mut exit_event_reader: MessageReader<AppExit>,
248    _non_send_marker: NonSendMarker,
249) {
250    // Drop all the windows that are waiting to be closed
251    windows_to_drop.clear();
252    for window in closing.iter() {
253        closing_event_writer.write(WindowClosing { window });
254    }
255    for window in closed.read() {
256        info!("Closing window {}", window);
257        // Guard to verify that the window is in fact actually gone,
258        // rather than having the component added
259        // and removed in the same frame.
260        if !window_entities.contains(window) {
261            WINIT_WINDOWS.with_borrow_mut(|winit_windows| {
262                if let Some(window) = winit_windows.remove_window(window) {
263                    // Keeping WindowWrapper that are dropped for one frame
264                    // Otherwise the last `Arc` of the window could be in the rendering thread, and dropped there
265                    // This would hang on macOS
266                    // Keeping the wrapper and dropping it next frame in this system ensure its dropped in the main thread
267                    windows_to_drop.push(window);
268                }
269            });
270            closed_event_writer.write(WindowClosed { window });
271        }
272    }
273
274    // On macOS, when exiting, we need to tell the rendering thread the windows are about to
275    // close to ensure that they are dropped on the main thread. Otherwise, the app will hang.
276    if !exit_event_reader.is_empty() {
277        exit_event_reader.clear();
278        for window in window_entities.iter() {
279            closing_event_writer.write(WindowClosing { window });
280        }
281    }
282}
283
284/// The cached state of the window so we can check which properties were changed from within the app.
285#[derive(Debug, Clone, Component, Deref, DerefMut)]
286pub(crate) struct CachedWindow(Window);
287
288/// The cached state of the window so we can check which properties were changed from within the app.
289#[derive(Debug, Clone, Component, Deref, DerefMut)]
290pub(crate) struct CachedCursorOptions(CursorOptions);
291
292/// Propagates changes from [`Window`] entities to the [`winit`] backend.
293///
294/// # Notes
295///
296/// - [`Window::present_mode`] and [`Window::composite_alpha_mode`] changes are handled by the `bevy_render` crate.
297/// - [`Window::transparent`] cannot be changed after the window is created.
298/// - [`Window::canvas`] cannot be changed after the window is created.
299/// - [`Window::focused`] cannot be manually changed to `false` after the window is created.
300pub(crate) fn changed_windows(
301    mut changed_windows: Query<(Entity, &mut Window, &mut CachedWindow), Changed<Window>>,
302    monitors: Res<WinitMonitors>,
303    mut window_resized: MessageWriter<WindowResized>,
304    _non_send_marker: NonSendMarker,
305) {
306    WINIT_WINDOWS.with_borrow(|winit_windows| {
307        for (entity, mut window, mut cache) in &mut changed_windows {
308            let Some(winit_window) = winit_windows.get_window(entity) else {
309                continue;
310            };
311
312            if window.title != cache.title {
313                winit_window.set_title(window.title.as_str());
314            }
315
316            if window.mode != cache.mode {
317                let new_mode = match window.mode {
318                    WindowMode::BorderlessFullscreen(monitor_selection) => {
319                        Some(Some(winit::window::Fullscreen::Borderless(select_monitor(
320                            &monitors,
321                            winit_window.primary_monitor(),
322                            winit_window.current_monitor(),
323                            &monitor_selection,
324                        ))))
325                    }
326                    WindowMode::Fullscreen(monitor_selection, video_mode_selection) => {
327                        let monitor = &select_monitor(
328                            &monitors,
329                            winit_window.primary_monitor(),
330                            winit_window.current_monitor(),
331                            &monitor_selection,
332                        )
333                        .unwrap_or_else(|| {
334                            panic!("Could not find monitor for {monitor_selection:?}")
335                        });
336
337                        if let Some(video_mode) = get_selected_videomode(monitor, &video_mode_selection)
338                        {
339                            Some(Some(winit::window::Fullscreen::Exclusive(video_mode)))
340                        } else {
341                            warn!(
342                                "Could not find valid fullscreen video mode for {:?} {:?}",
343                                monitor_selection, video_mode_selection
344                            );
345                            None
346                        }
347                    }
348                    WindowMode::Windowed => Some(None),
349                };
350
351                if let Some(new_mode) = new_mode
352                    && winit_window.fullscreen() != new_mode {
353                        winit_window.set_fullscreen(new_mode);
354                    }
355            }
356
357            if window.resolution != cache.resolution {
358                let mut physical_size = PhysicalSize::new(
359                    window.resolution.physical_width(),
360                    window.resolution.physical_height(),
361                );
362
363                let cached_physical_size = PhysicalSize::new(
364                    cache.physical_width(),
365                    cache.physical_height(),
366                );
367
368                let base_scale_factor = window.resolution.base_scale_factor();
369
370                // Note: this may be different from `winit`'s base scale factor if
371                // `scale_factor_override` is set to Some(f32)
372                let scale_factor = window.scale_factor();
373                let cached_scale_factor = cache.scale_factor();
374
375                // Check and update `winit`'s physical size only if the window is not maximized
376                if scale_factor != cached_scale_factor && !winit_window.is_maximized() {
377                    let logical_size =
378                        if let Some(cached_factor) = cache.resolution.scale_factor_override() {
379                            physical_size.to_logical::<f32>(cached_factor as f64)
380                        } else {
381                            physical_size.to_logical::<f32>(base_scale_factor as f64)
382                        };
383
384                    // Scale factor changed, updating physical and logical size
385                    if let Some(forced_factor) = window.resolution.scale_factor_override() {
386                        // This window is overriding the OS-suggested DPI, so its physical size
387                        // should be set based on the overriding value. Its logical size already
388                        // incorporates any resize constraints.
389                        physical_size = logical_size.to_physical::<u32>(forced_factor as f64);
390                    } else {
391                        physical_size = logical_size.to_physical::<u32>(base_scale_factor as f64);
392                    }
393                }
394
395                if physical_size != cached_physical_size
396                    && let Some(new_physical_size) = winit_window.request_inner_size(physical_size) {
397                        react_to_resize(entity, &mut window, new_physical_size, &mut window_resized);
398                    }
399            }
400
401            if window.physical_cursor_position() != cache.physical_cursor_position()
402                && let Some(physical_position) = window.physical_cursor_position() {
403                    let position = PhysicalPosition::new(physical_position.x, physical_position.y);
404
405                    if let Err(err) = winit_window.set_cursor_position(position) {
406                        error!("could not set cursor position: {}", err);
407                    }
408                }
409
410            if window.decorations != cache.decorations
411                && window.decorations != winit_window.is_decorated()
412            {
413                winit_window.set_decorations(window.decorations);
414            }
415
416            if window.resizable != cache.resizable
417                && window.resizable != winit_window.is_resizable()
418            {
419                winit_window.set_resizable(window.resizable);
420            }
421
422            if window.enabled_buttons != cache.enabled_buttons {
423                winit_window.set_enabled_buttons(convert_enabled_buttons(window.enabled_buttons));
424            }
425
426            if window.resize_constraints != cache.resize_constraints {
427                let constraints = window.resize_constraints.check_constraints();
428                let min_inner_size = LogicalSize {
429                    width: constraints.min_width,
430                    height: constraints.min_height,
431                };
432                let max_inner_size = LogicalSize {
433                    width: constraints.max_width,
434                    height: constraints.max_height,
435                };
436
437                winit_window.set_min_inner_size(Some(min_inner_size));
438                if constraints.max_width.is_finite() && constraints.max_height.is_finite() {
439                    winit_window.set_max_inner_size(Some(max_inner_size));
440                }
441            }
442
443            if window.position != cache.position
444                && let Some(position) = crate::winit_window_position(
445                    &window.position,
446                    &window.resolution,
447                    &monitors,
448                    winit_window.primary_monitor(),
449                    winit_window.current_monitor(),
450                ) {
451                    let should_set = match winit_window.outer_position() {
452                        Ok(current_position) => current_position != position,
453                        _ => true,
454                    };
455
456                    if should_set {
457                        winit_window.set_outer_position(position);
458                    }
459                }
460
461            if let Some(maximized) = window.internal.take_maximize_request() {
462                winit_window.set_maximized(maximized);
463            }
464
465            if let Some(minimized) = window.internal.take_minimize_request() {
466                winit_window.set_minimized(minimized);
467            }
468
469            if window.internal.take_move_request()
470                && let Err(e) = winit_window.drag_window() {
471                    warn!("Winit returned an error while attempting to drag the window: {e}");
472                }
473
474            if let Some(resize_direction) = window.internal.take_resize_request()
475                && let Err(e) =
476                    winit_window.drag_resize_window(convert_resize_direction(resize_direction))
477                {
478                    warn!("Winit returned an error while attempting to drag resize the window: {e}");
479                }
480
481            if window.focused != cache.focused && window.focused {
482                winit_window.focus_window();
483            }
484
485            if window.window_level != cache.window_level {
486                winit_window.set_window_level(convert_window_level(window.window_level));
487            }
488
489            // Currently unsupported changes
490            if window.transparent != cache.transparent {
491                window.transparent = cache.transparent;
492                warn!("Winit does not currently support updating transparency after window creation.");
493            }
494
495            #[cfg(target_arch = "wasm32")]
496            if window.canvas != cache.canvas {
497                window.canvas.clone_from(&cache.canvas);
498                warn!(
499                    "Bevy currently doesn't support modifying the window canvas after initialization."
500                );
501            }
502
503            if window.ime_enabled != cache.ime_enabled {
504                winit_window.set_ime_allowed(window.ime_enabled);
505            }
506
507            if window.ime_position != cache.ime_position {
508                winit_window.set_ime_cursor_area(
509                    LogicalPosition::new(window.ime_position.x, window.ime_position.y),
510                    PhysicalSize::new(10, 10),
511                );
512            }
513
514            if window.window_theme != cache.window_theme {
515                winit_window.set_theme(window.window_theme.map(convert_window_theme));
516            }
517
518            if window.visible != cache.visible {
519                winit_window.set_visible(window.visible);
520            }
521
522            #[cfg(target_os = "ios")]
523            {
524                if window.recognize_pinch_gesture != cache.recognize_pinch_gesture {
525                    winit_window.recognize_pinch_gesture(window.recognize_pinch_gesture);
526                }
527                if window.recognize_rotation_gesture != cache.recognize_rotation_gesture {
528                    winit_window.recognize_rotation_gesture(window.recognize_rotation_gesture);
529                }
530                if window.recognize_doubletap_gesture != cache.recognize_doubletap_gesture {
531                    winit_window.recognize_doubletap_gesture(window.recognize_doubletap_gesture);
532                }
533                if window.recognize_pan_gesture != cache.recognize_pan_gesture {
534                    match (
535                        window.recognize_pan_gesture,
536                        cache.recognize_pan_gesture,
537                    ) {
538                        (Some(_), Some(_)) => {
539                            warn!("Bevy currently doesn't support modifying PanGesture number of fingers recognition. Please disable it before re-enabling it with the new number of fingers");
540                        }
541                        (Some((min, max)), _) => winit_window.recognize_pan_gesture(true, min, max),
542                        _ => winit_window.recognize_pan_gesture(false, 0, 0),
543                    }
544                }
545
546                if window.prefers_home_indicator_hidden != cache.prefers_home_indicator_hidden {
547                    winit_window
548                        .set_prefers_home_indicator_hidden(window.prefers_home_indicator_hidden);
549                }
550                if window.prefers_status_bar_hidden != cache.prefers_status_bar_hidden {
551                    winit_window.set_prefers_status_bar_hidden(window.prefers_status_bar_hidden);
552                }
553                if window.preferred_screen_edges_deferring_system_gestures
554                    != cache
555                        .preferred_screen_edges_deferring_system_gestures
556                {
557                    use crate::converters::convert_screen_edge;
558                    let preferred_edge =
559                        convert_screen_edge(window.preferred_screen_edges_deferring_system_gestures);
560                    winit_window.set_preferred_screen_edges_deferring_system_gestures(preferred_edge);
561                }
562            }
563            **cache = window.clone();
564        }
565    });
566}
567
568pub(crate) fn changed_cursor_options(
569    mut changed_windows: Query<
570        (
571            Entity,
572            &Window,
573            &mut CursorOptions,
574            &mut CachedCursorOptions,
575        ),
576        Changed<CursorOptions>,
577    >,
578    _non_send_marker: NonSendMarker,
579) {
580    WINIT_WINDOWS.with_borrow(|winit_windows| {
581        for (entity, window, mut cursor_options, mut cache) in &mut changed_windows {
582            // This system already only runs when the cursor options change, so we need to bypass change detection or the next frame will also run this system
583            let cursor_options = cursor_options.bypass_change_detection();
584            let Some(winit_window) = winit_windows.get_window(entity) else {
585                continue;
586            };
587            // Don't check the cache for the grab mode. It can change through external means, leaving the cache outdated.
588            if let Err(err) =
589                crate::winit_windows::attempt_grab(winit_window, cursor_options.grab_mode)
590            {
591                warn!(
592                    "Could not set cursor grab mode for window {}: {}",
593                    window.title, err
594                );
595                cursor_options.grab_mode = cache.grab_mode;
596            } else {
597                cache.grab_mode = cursor_options.grab_mode;
598            }
599
600            if cursor_options.visible != cache.visible {
601                winit_window.set_cursor_visible(cursor_options.visible);
602                cache.visible = cursor_options.visible;
603            }
604
605            if cursor_options.hit_test != cache.hit_test {
606                if let Err(err) = winit_window.set_cursor_hittest(cursor_options.hit_test) {
607                    warn!(
608                        "Could not set cursor hit test for window {}: {}",
609                        window.title, err
610                    );
611                    cursor_options.hit_test = cache.hit_test;
612                } else {
613                    cache.hit_test = cursor_options.hit_test;
614                }
615            }
616        }
617    });
618}
619
620/// This keeps track of which keys are pressed on each window.
621/// When a window is unfocused, this is used to send key release events for all the currently held keys.
622#[derive(Default, Component)]
623pub struct WinitWindowPressedKeys(pub(crate) HashMap<KeyCode, Key>);