Skip to content

Commit b731eba

Browse files
committed
Allow closing windows at runtime (#3575)
# Objective Fixes #3180, builds from #2898 ## Solution Support requesting a window to be closed and closing a window in `bevy_window`, and handle this in `bevy_winit`. This is a stopgap until we move to windows as entites, which I'm sure I'll get around to eventually. ## Changelog ### Added - `Window::close` to allow closing windows. - `WindowClosed` to allow reacting to windows being closed. ### Changed Replaced `bevy::system::exit_on_esc_system` with `bevy::window::close_on_esc`. ## Fixed The app no longer exits when any window is closed. This difference is only observable when there are multiple windows. ## Migration Guide `bevy::input::system::exit_on_esc_system` has been removed. Use `bevy::window::close_on_esc` instead. `CloseWindow` has been removed. Use `Window::close` instead. The `Close` variant has been added to `WindowCommand`. Handle this by closing the relevant window.
1 parent 5585308 commit b731eba

File tree

16 files changed

+178
-65
lines changed

16 files changed

+178
-65
lines changed

crates/bevy_input/src/lib.rs

-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ pub mod gamepad;
33
mod input;
44
pub mod keyboard;
55
pub mod mouse;
6-
pub mod system;
76
pub mod touch;
87

98
pub use axis::*;

crates/bevy_input/src/system.rs

-22
This file was deleted.

crates/bevy_render/src/view/window.rs

+10-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use crate::{
77
use bevy_app::{App, Plugin};
88
use bevy_ecs::prelude::*;
99
use bevy_utils::{tracing::debug, HashMap, HashSet};
10-
use bevy_window::{PresentMode, RawWindowHandleWrapper, WindowId, Windows};
10+
use bevy_window::{PresentMode, RawWindowHandleWrapper, WindowClosed, WindowId, Windows};
1111
use std::ops::{Deref, DerefMut};
1212
use wgpu::TextureFormat;
1313

@@ -67,8 +67,12 @@ impl DerefMut for ExtractedWindows {
6767
}
6868
}
6969

70-
fn extract_windows(mut render_world: ResMut<RenderWorld>, windows: Res<Windows>) {
71-
let mut extracted_windows = render_world.resource_mut::<ExtractedWindows>();
70+
fn extract_windows(
71+
mut render_world: ResMut<RenderWorld>,
72+
mut closed: EventReader<WindowClosed>,
73+
windows: Res<Windows>,
74+
) {
75+
let mut extracted_windows = render_world.get_resource_mut::<ExtractedWindows>().unwrap();
7276
for window in windows.iter() {
7377
let (new_width, new_height) = (
7478
window.physical_width().max(1),
@@ -105,6 +109,9 @@ fn extract_windows(mut render_world: ResMut<RenderWorld>, windows: Res<Windows>)
105109
extracted_window.physical_height = new_height;
106110
}
107111
}
112+
for closed_window in closed.iter() {
113+
extracted_windows.remove(&closed_window.id);
114+
}
108115
}
109116

110117
#[derive(Default)]

crates/bevy_window/Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ bevy_app = { path = "../bevy_app", version = "0.8.0-dev" }
1414
bevy_ecs = { path = "../bevy_ecs", version = "0.8.0-dev" }
1515
bevy_math = { path = "../bevy_math", version = "0.8.0-dev" }
1616
bevy_utils = { path = "../bevy_utils", version = "0.8.0-dev" }
17+
# Used for close_on_esc
18+
bevy_input = { path = "../bevy_input", version = "0.8.0-dev" }
1719
raw-window-handle = "0.4.2"
1820

1921
# other

crates/bevy_window/src/event.rs

+22-7
Original file line numberDiff line numberDiff line change
@@ -25,22 +25,37 @@ pub struct CreateWindow {
2525
#[derive(Debug, Clone)]
2626
pub struct RequestRedraw;
2727

28-
/// An event that indicates a window should be closed.
28+
/// An event that is sent whenever a new window is created.
29+
///
30+
/// To create a new window, send a [`CreateWindow`] event - this
31+
/// event will be sent in the handler for that event.
2932
#[derive(Debug, Clone)]
30-
pub struct CloseWindow {
33+
pub struct WindowCreated {
3134
pub id: WindowId,
3235
}
3336

34-
/// An event that is sent whenever a new window is created.
37+
/// An event that is sent whenever the operating systems requests that a window
38+
/// be closed. This will be sent when the close button of the window is pressed.
39+
///
40+
/// If the default [`WindowPlugin`] is used, these events are handled
41+
/// by [closing] the corresponding [`Window`].
42+
/// To disable this behaviour, set `close_when_requested` on the [`WindowPlugin`]
43+
/// to `false`.
44+
///
45+
/// [`WindowPlugin`]: crate::WindowPlugin
46+
/// [`Window`]: crate::Window
47+
/// [closing]: crate::Window::close
3548
#[derive(Debug, Clone)]
36-
pub struct WindowCreated {
49+
pub struct WindowCloseRequested {
3750
pub id: WindowId,
3851
}
3952

40-
/// An event that is sent whenever a close was requested for a window. For example: when the "close"
41-
/// button is pressed on a window.
53+
/// An event that is sent whenever a window is closed. This will be sent by the
54+
/// handler for [`Window::close`].
55+
///
56+
/// [`Window::close`]: crate::Window::close
4257
#[derive(Debug, Clone)]
43-
pub struct WindowCloseRequested {
58+
pub struct WindowClosed {
4459
pub id: WindowId,
4560
}
4661

crates/bevy_window/src/lib.rs

+27-5
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,34 @@ use bevy_app::prelude::*;
2424
use bevy_ecs::{event::Events, schedule::SystemLabel};
2525

2626
pub struct WindowPlugin {
27+
/// Whether to create a window when added.
28+
///
29+
/// Note that if there are no windows, by default the App will exit,
30+
/// due to [`exit_on_all_closed`].
2731
pub add_primary_window: bool,
28-
pub exit_on_close: bool,
32+
/// Whether to exit the app when there are no open windows.
33+
/// If disabling this, ensure that you send the [`bevy_app::AppExit`]
34+
/// event when the app should exit. If this does not occur, you will
35+
/// create 'headless' processes (processes without windows), which may
36+
/// surprise your users. It is recommended to leave this setting as `true`.
37+
///
38+
/// If true, this plugin will add [`exit_on_all_closed`] to [`CoreStage::Update`].
39+
pub exit_on_all_closed: bool,
40+
/// Whether to close windows when they are requested to be closed (i.e.
41+
/// when the close button is pressed)
42+
///
43+
/// If true, this plugin will add [`close_when_requested`] to [`CoreStage::Update`].
44+
/// If this system (or a replacement) is not running, the close button will have no effect.
45+
/// This may surprise your users. It is recommended to leave this setting as `true`.
46+
pub close_when_requested: bool,
2947
}
3048

3149
impl Default for WindowPlugin {
3250
fn default() -> Self {
3351
WindowPlugin {
3452
add_primary_window: true,
35-
exit_on_close: true,
53+
exit_on_all_closed: true,
54+
close_when_requested: true,
3655
}
3756
}
3857
}
@@ -42,9 +61,9 @@ impl Plugin for WindowPlugin {
4261
app.add_event::<WindowResized>()
4362
.add_event::<CreateWindow>()
4463
.add_event::<WindowCreated>()
64+
.add_event::<WindowClosed>()
4565
.add_event::<WindowCloseRequested>()
4666
.add_event::<RequestRedraw>()
47-
.add_event::<CloseWindow>()
4867
.add_event::<CursorMoved>()
4968
.add_event::<CursorEntered>()
5069
.add_event::<CursorLeft>()
@@ -69,8 +88,11 @@ impl Plugin for WindowPlugin {
6988
});
7089
}
7190

72-
if self.exit_on_close {
73-
app.add_system(exit_on_window_close_system);
91+
if self.exit_on_all_closed {
92+
app.add_system(exit_on_all_closed);
93+
}
94+
if self.close_when_requested {
95+
app.add_system(close_when_requested);
7496
}
7597
}
7698
}

crates/bevy_window/src/system.rs

+52-7
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,57 @@
1-
use crate::WindowCloseRequested;
1+
use crate::{Window, WindowCloseRequested, WindowFocused, WindowId, Windows};
2+
23
use bevy_app::AppExit;
3-
use bevy_ecs::event::{EventReader, EventWriter};
4+
use bevy_ecs::prelude::*;
5+
use bevy_input::{keyboard::KeyCode, Input};
46

5-
pub fn exit_on_window_close_system(
6-
mut app_exit_events: EventWriter<AppExit>,
7-
mut window_close_requested_events: EventReader<WindowCloseRequested>,
8-
) {
9-
if window_close_requested_events.iter().next().is_some() {
7+
/// Exit the application when there are no open windows.
8+
///
9+
/// This system is added by the [`WindowPlugin`] in the default configuration.
10+
/// To disable this behaviour, set `close_when_requested` (on the [`WindowPlugin`]) to `false`.
11+
/// Ensure that you read the caveats documented on that field if doing so.
12+
///
13+
/// [`WindowPlugin`]: crate::WindowPlugin
14+
pub fn exit_on_all_closed(mut app_exit_events: EventWriter<AppExit>, windows: Res<Windows>) {
15+
if windows.iter().count() == 0 {
1016
app_exit_events.send(AppExit);
1117
}
1218
}
19+
20+
/// Close windows in response to [`WindowCloseRequested`] (e.g. when the close button is pressed).
21+
///
22+
/// This system is added by the [`WindowPlugin`] in the default configuration.
23+
/// To disable this behaviour, set `close_when_requested` (on the [`WindowPlugin`]) to `false`.
24+
/// Ensure that you read the caveats documented on that field if doing so.
25+
///
26+
/// [`WindowPlugin`]: crate::WindowPlugin
27+
pub fn close_when_requested(
28+
mut windows: ResMut<Windows>,
29+
mut closed: EventReader<WindowCloseRequested>,
30+
) {
31+
for event in closed.iter() {
32+
windows.get_mut(event.id).map(Window::close);
33+
}
34+
}
35+
36+
/// Close the focused window whenever the escape key (<kbd>Esc</kbd>) is pressed
37+
///
38+
/// This is useful for examples or prototyping.
39+
pub fn close_on_esc(
40+
mut focused: Local<Option<WindowId>>,
41+
mut focused_events: EventReader<WindowFocused>,
42+
mut windows: ResMut<Windows>,
43+
input: Res<Input<KeyCode>>,
44+
) {
45+
// TODO: Track this in e.g. a resource to ensure consistent behaviour across similar systems
46+
for event in focused_events.iter() {
47+
*focused = event.focused.then(|| event.id);
48+
}
49+
50+
if let Some(focused) = &*focused {
51+
if input.just_pressed(KeyCode::Escape) {
52+
if let Some(window) = windows.get_mut(*focused) {
53+
window.close();
54+
}
55+
}
56+
}
57+
}

crates/bevy_window/src/window.rs

+16
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ pub enum WindowCommand {
219219
SetResizeConstraints {
220220
resize_constraints: WindowResizeConstraints,
221221
},
222+
Close,
222223
}
223224

224225
/// Defines the way a window is displayed
@@ -571,6 +572,21 @@ impl Window {
571572
});
572573
}
573574

575+
/// Close the operating system window corresponding to this [`Window`].
576+
/// This will also lead to this [`Window`] being removed from the
577+
/// [`Windows`] resource.
578+
///
579+
/// If the default [`WindowPlugin`] is used, when no windows are
580+
/// open, the [app will exit](bevy_app::AppExit).
581+
/// To disable this behaviour, set `exit_on_all_closed` on the [`WindowPlugin`]
582+
/// to `false`
583+
///
584+
/// [`Windows`]: crate::Windows
585+
/// [`WindowPlugin`]: crate::WindowPlugin
586+
pub fn close(&mut self) {
587+
self.command_queue.push(WindowCommand::Close);
588+
}
589+
574590
#[inline]
575591
pub fn drain_commands(&mut self) -> impl Iterator<Item = WindowCommand> + '_ {
576592
self.command_queue.drain(..)

crates/bevy_window/src/windows.rs

+4
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,8 @@ impl Windows {
7070
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Window> {
7171
self.windows.values_mut()
7272
}
73+
74+
pub fn remove(&mut self, id: WindowId) -> Option<Window> {
75+
self.windows.remove(&id)
76+
}
7377
}

crates/bevy_winit/src/lib.rs

+33-15
Original file line numberDiff line numberDiff line change
@@ -2,39 +2,38 @@ mod converters;
22
mod winit_config;
33
mod winit_windows;
44

5-
use bevy_input::{
6-
keyboard::KeyboardInput,
7-
mouse::{MouseButtonInput, MouseMotion, MouseScrollUnit, MouseWheel},
8-
touch::TouchInput,
9-
};
105
pub use winit_config::*;
116
pub use winit_windows::*;
127

138
use bevy_app::{App, AppExit, CoreStage, Plugin};
9+
use bevy_ecs::prelude::*;
1410
use bevy_ecs::{
15-
event::{EventWriter, Events, ManualEventReader},
16-
schedule::ParallelSystemDescriptorCoercion,
17-
system::{NonSend, ResMut},
11+
event::{Events, ManualEventReader},
1812
world::World,
1913
};
14+
use bevy_input::{
15+
keyboard::KeyboardInput,
16+
mouse::{MouseButtonInput, MouseMotion, MouseScrollUnit, MouseWheel},
17+
touch::TouchInput,
18+
};
2019
use bevy_math::{ivec2, DVec2, Vec2};
2120
use bevy_utils::{
22-
tracing::{error, trace, warn},
21+
tracing::{error, info, trace, warn},
2322
Instant,
2423
};
2524
use bevy_window::{
2625
CreateWindow, CursorEntered, CursorLeft, CursorMoved, FileDragAndDrop, ModifiesWindows,
2726
ReceivedCharacter, RequestRedraw, WindowBackendScaleFactorChanged, WindowCloseRequested,
28-
WindowCreated, WindowFocused, WindowMoved, WindowResized, WindowScaleFactorChanged, Windows,
27+
WindowClosed, WindowCreated, WindowFocused, WindowMoved, WindowResized,
28+
WindowScaleFactorChanged, Windows,
2929
};
30+
3031
use winit::{
31-
dpi::PhysicalPosition,
32+
dpi::{LogicalSize, PhysicalPosition},
3233
event::{self, DeviceEvent, Event, StartCause, WindowEvent},
3334
event_loop::{ControlFlow, EventLoop, EventLoopWindowTarget},
3435
};
3536

36-
use winit::dpi::LogicalSize;
37-
3837
#[derive(Default)]
3938
pub struct WinitPlugin;
4039

@@ -51,10 +50,12 @@ impl Plugin for WinitPlugin {
5150
}
5251

5352
fn change_window(
54-
winit_windows: NonSend<WinitWindows>,
53+
mut winit_windows: NonSendMut<WinitWindows>,
5554
mut windows: ResMut<Windows>,
5655
mut window_dpi_changed_events: EventWriter<WindowScaleFactorChanged>,
56+
mut window_close_events: EventWriter<WindowClosed>,
5757
) {
58+
let mut removed_windows = vec![];
5859
for bevy_window in windows.iter_mut() {
5960
let id = bevy_window.id();
6061
for command in bevy_window.drain_commands() {
@@ -166,9 +167,25 @@ fn change_window(
166167
window.set_max_inner_size(Some(max_inner_size));
167168
}
168169
}
170+
bevy_window::WindowCommand::Close => {
171+
// Since we have borrowed `windows` to iterate through them, we can't remove the window from it.
172+
// Add the removal requests to a queue to solve this
173+
removed_windows.push(id);
174+
// No need to run any further commands - this drops the rest of the commands, although the `bevy_window::Window` will be dropped later anyway
175+
break;
176+
}
169177
}
170178
}
171179
}
180+
if !removed_windows.is_empty() {
181+
for id in removed_windows {
182+
// Close the OS window. (The `Drop` impl actually closes the window)
183+
let _ = winit_windows.remove_window(id);
184+
// Clean up our own data structures
185+
windows.remove(id);
186+
window_close_events.send(WindowClosed { id });
187+
}
188+
}
172189
}
173190

174191
fn run<F>(event_loop: EventLoop<()>, event_handler: F) -> !
@@ -316,7 +333,8 @@ pub fn winit_runner_with(mut app: App) {
316333
let window = if let Some(window) = windows.get_mut(window_id) {
317334
window
318335
} else {
319-
warn!("Skipped event for unknown Window Id {:?}", winit_window_id);
336+
// If we're here, this window was previously opened
337+
info!("Skipped event for closed window: {:?}", window_id);
320338
return;
321339
};
322340
winit_state.low_power_event = true;

crates/bevy_winit/src/winit_windows.rs

+6
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,12 @@ impl WinitWindows {
181181
pub fn get_window_id(&self, id: winit::window::WindowId) -> Option<WindowId> {
182182
self.winit_to_window_id.get(&id).cloned()
183183
}
184+
185+
pub fn remove_window(&mut self, id: WindowId) -> Option<winit::window::Window> {
186+
let winit_id = self.window_id_to_winit.remove(&id)?;
187+
// Don't remove from winit_to_window_id, to track that we used to know about this winit window
188+
self.windows.remove(&winit_id)
189+
}
184190
}
185191

186192
pub fn get_fitting_videomode(

0 commit comments

Comments
 (0)