bevy_winit/
system.rs

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