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