bevy_winit/
system.rs

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