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#[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
127pub(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
147pub 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 #[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 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 if !window_entities.contains(window) {
242 if let Some(window) = winit_windows.remove_window(window) {
243 windows_to_drop.push(window);
248 }
249 closed_events.send(WindowClosed { window });
250 }
251 }
252
253 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#[derive(Debug, Clone, Component)]
265pub struct CachedWindow {
266 pub window: Window,
267}
268
269pub(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 let scale_factor = window.scale_factor();
359 let cached_scale_factor = cache.window.scale_factor();
360
361 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 if let Some(forced_factor) = window.resolution.scale_factor_override() {
372 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 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}