Skip to main content

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