Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Merged by Bors] - Allow closing windows at runtime #3575

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion crates/bevy_input/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ pub mod gamepad;
mod input;
pub mod keyboard;
pub mod mouse;
pub mod system;
pub mod touch;

pub use axis::*;
Expand Down
22 changes: 0 additions & 22 deletions crates/bevy_input/src/system.rs

This file was deleted.

13 changes: 10 additions & 3 deletions crates/bevy_render/src/view/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::{
use bevy_app::{App, Plugin};
use bevy_ecs::prelude::*;
use bevy_utils::{tracing::debug, HashMap, HashSet};
use bevy_window::{PresentMode, RawWindowHandleWrapper, WindowId, Windows};
use bevy_window::{PresentMode, RawWindowHandleWrapper, WindowClosed, WindowId, Windows};
use std::ops::{Deref, DerefMut};
use wgpu::TextureFormat;

Expand Down Expand Up @@ -67,8 +67,12 @@ impl DerefMut for ExtractedWindows {
}
}

fn extract_windows(mut render_world: ResMut<RenderWorld>, windows: Res<Windows>) {
let mut extracted_windows = render_world.resource_mut::<ExtractedWindows>();
fn extract_windows(
mut render_world: ResMut<RenderWorld>,
mut closed: EventReader<WindowClosed>,
windows: Res<Windows>,
) {
let mut extracted_windows = render_world.get_resource_mut::<ExtractedWindows>().unwrap();
for window in windows.iter() {
let (new_width, new_height) = (
window.physical_width().max(1),
Expand Down Expand Up @@ -105,6 +109,9 @@ fn extract_windows(mut render_world: ResMut<RenderWorld>, windows: Res<Windows>)
extracted_window.physical_height = new_height;
}
}
for closed_window in closed.iter() {
extracted_windows.remove(&closed_window.id);
}
}

#[derive(Default)]
Expand Down
2 changes: 2 additions & 0 deletions crates/bevy_window/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ bevy_app = { path = "../bevy_app", version = "0.8.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.8.0-dev" }
bevy_math = { path = "../bevy_math", version = "0.8.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.8.0-dev" }
# Used for close_on_esc
bevy_input = { path = "../bevy_input", version = "0.8.0-dev" }
raw-window-handle = "0.4.2"

# other
Expand Down
29 changes: 22 additions & 7 deletions crates/bevy_window/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,37 @@ pub struct CreateWindow {
#[derive(Debug, Clone)]
pub struct RequestRedraw;

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

/// An event that is sent whenever a new window is created.
/// An event that is sent whenever the operating systems requests that a window
/// be closed. This will be sent when the close button of the window is pressed.
///
/// If the default [`WindowPlugin`] is used, these events are handled
/// by [closing] the corresponding [`Window`].
/// To disable this behaviour, set `close_when_requested` on the [`WindowPlugin`]
/// to `false`.
///
/// [`WindowPlugin`]: crate::WindowPlugin
/// [`Window`]: crate::Window
/// [closing]: crate::Window::close
#[derive(Debug, Clone)]
pub struct WindowCreated {
pub struct WindowCloseRequested {
pub id: WindowId,
}

/// An event that is sent whenever a close was requested for a window. For example: when the "close"
/// button is pressed on a window.
/// An event that is sent whenever a window is closed. This will be sent by the
/// handler for [`Window::close`].
///
/// [`Window::close`]: crate::Window::close
#[derive(Debug, Clone)]
pub struct WindowCloseRequested {
pub struct WindowClosed {
pub id: WindowId,
}

Expand Down
32 changes: 27 additions & 5 deletions crates/bevy_window/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,34 @@ use bevy_app::prelude::*;
use bevy_ecs::{event::Events, schedule::SystemLabel};

pub struct WindowPlugin {
/// Whether to create a window when added.
///
/// Note that if there are no windows, by default the App will exit,
/// due to [`exit_on_all_closed`].
pub add_primary_window: bool,
pub exit_on_close: bool,
/// Whether to exit the app when there are no open windows.
/// If disabling this, ensure that you send the [`bevy_app::AppExit`]
/// event when the app should exit. If this does not occur, you will
/// create 'headless' processes (processes without windows), which may
/// surprise your users. It is recommended to leave this setting as `true`.
///
/// If true, this plugin will add [`exit_on_all_closed`] to [`CoreStage::Update`].
pub exit_on_all_closed: bool,
/// Whether to close windows when they are requested to be closed (i.e.
/// when the close button is pressed)
///
/// If true, this plugin will add [`close_when_requested`] to [`CoreStage::Update`].
/// If this system (or a replacement) is not running, the close button will have no effect.
/// This may surprise your users. It is recommended to leave this setting as `true`.
pub close_when_requested: bool,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this field should exist. I agree with you that it's unintuitive behavior to set this to false; when would app developers want to do this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example, if the user has unsaved changes, it's traditional to give an 'unsaved changes' warning window.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, got it. In that case, this should be exposed as a resource: that behavior needs to be able to be changed dynamically.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really? If a developer wants this functionality, they can easily enough add it themselves. This is merely making sure the default behaviour is correct, and we give an escape hatch for when people need more complicated logic.

That is, I don't really want to encourage disabling this, but I want to make the option available.

}

impl Default for WindowPlugin {
fn default() -> Self {
WindowPlugin {
add_primary_window: true,
exit_on_close: true,
exit_on_all_closed: true,
close_when_requested: true,
}
}
}
Expand All @@ -42,9 +61,9 @@ impl Plugin for WindowPlugin {
app.add_event::<WindowResized>()
.add_event::<CreateWindow>()
.add_event::<WindowCreated>()
.add_event::<WindowClosed>()
.add_event::<WindowCloseRequested>()
.add_event::<RequestRedraw>()
.add_event::<CloseWindow>()
.add_event::<CursorMoved>()
.add_event::<CursorEntered>()
.add_event::<CursorLeft>()
Expand All @@ -69,8 +88,11 @@ impl Plugin for WindowPlugin {
});
}

if self.exit_on_close {
app.add_system(exit_on_window_close_system);
if self.exit_on_all_closed {
app.add_system(exit_on_all_closed);
}
if self.close_when_requested {
app.add_system(close_when_requested);
}
}
}
Expand Down
59 changes: 52 additions & 7 deletions crates/bevy_window/src/system.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,57 @@
use crate::WindowCloseRequested;
use crate::{Window, WindowCloseRequested, WindowFocused, WindowId, Windows};

use bevy_app::AppExit;
use bevy_ecs::event::{EventReader, EventWriter};
use bevy_ecs::prelude::*;
use bevy_input::{keyboard::KeyCode, Input};

pub fn exit_on_window_close_system(
mut app_exit_events: EventWriter<AppExit>,
mut window_close_requested_events: EventReader<WindowCloseRequested>,
) {
if window_close_requested_events.iter().next().is_some() {
/// Exit the application when there are no open windows.
///
/// This system is added by the [`WindowPlugin`] in the default configuration.
/// To disable this behaviour, set `close_when_requested` (on the [`WindowPlugin`]) to `false`.
/// Ensure that you read the caveats documented on that field if doing so.
///
/// [`WindowPlugin`]: crate::WindowPlugin
pub fn exit_on_all_closed(mut app_exit_events: EventWriter<AppExit>, windows: Res<Windows>) {
if windows.iter().count() == 0 {
app_exit_events.send(AppExit);
}
}

/// Close windows in response to [`WindowCloseRequested`] (e.g. when the close button is pressed).
///
/// This system is added by the [`WindowPlugin`] in the default configuration.
/// To disable this behaviour, set `close_when_requested` (on the [`WindowPlugin`]) to `false`.
/// Ensure that you read the caveats documented on that field if doing so.
///
/// [`WindowPlugin`]: crate::WindowPlugin
pub fn close_when_requested(
mut windows: ResMut<Windows>,
mut closed: EventReader<WindowCloseRequested>,
) {
for event in closed.iter() {
windows.get_mut(event.id).map(Window::close);
}
}

/// Close the focused window whenever the escape key (<kbd>Esc</kbd>) is pressed
///
/// This is useful for examples or prototyping.
pub fn close_on_esc(
mut focused: Local<Option<WindowId>>,
mut focused_events: EventReader<WindowFocused>,
mut windows: ResMut<Windows>,
input: Res<Input<KeyCode>>,
) {
// TODO: Track this in e.g. a resource to ensure consistent behaviour across similar systems
for event in focused_events.iter() {
*focused = event.focused.then(|| event.id);
}

if let Some(focused) = &*focused {
if input.just_pressed(KeyCode::Escape) {
if let Some(window) = windows.get_mut(*focused) {
window.close();
}
}
}
}
16 changes: 16 additions & 0 deletions crates/bevy_window/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ pub enum WindowCommand {
SetResizeConstraints {
resize_constraints: WindowResizeConstraints,
},
Close,
}

/// Defines the way a window is displayed
Expand Down Expand Up @@ -571,6 +572,21 @@ impl Window {
});
}

/// Close the operating system window corresponding to this [`Window`].
/// This will also lead to this [`Window`] being removed from the
/// [`Windows`] resource.
///
/// If the default [`WindowPlugin`] is used, when no windows are
/// open, the [app will exit](bevy_app::AppExit).
/// To disable this behaviour, set `exit_on_all_closed` on the [`WindowPlugin`]
/// to `false`
///
/// [`Windows`]: crate::Windows
/// [`WindowPlugin`]: crate::WindowPlugin
pub fn close(&mut self) {
self.command_queue.push(WindowCommand::Close);
}

#[inline]
pub fn drain_commands(&mut self) -> impl Iterator<Item = WindowCommand> + '_ {
self.command_queue.drain(..)
Expand Down
4 changes: 4 additions & 0 deletions crates/bevy_window/src/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,8 @@ impl Windows {
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Window> {
self.windows.values_mut()
}

pub fn remove(&mut self, id: WindowId) -> Option<Window> {
self.windows.remove(&id)
}
}
48 changes: 33 additions & 15 deletions crates/bevy_winit/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,38 @@ mod converters;
mod winit_config;
mod winit_windows;

use bevy_input::{
keyboard::KeyboardInput,
mouse::{MouseButtonInput, MouseMotion, MouseScrollUnit, MouseWheel},
touch::TouchInput,
};
pub use winit_config::*;
pub use winit_windows::*;

use bevy_app::{App, AppExit, CoreStage, Plugin};
use bevy_ecs::prelude::*;
use bevy_ecs::{
event::{EventWriter, Events, ManualEventReader},
schedule::ParallelSystemDescriptorCoercion,
system::{NonSend, ResMut},
event::{Events, ManualEventReader},
world::World,
};
use bevy_input::{
keyboard::KeyboardInput,
mouse::{MouseButtonInput, MouseMotion, MouseScrollUnit, MouseWheel},
touch::TouchInput,
};
use bevy_math::{ivec2, DVec2, Vec2};
use bevy_utils::{
tracing::{error, trace, warn},
tracing::{error, info, trace, warn},
Instant,
};
use bevy_window::{
CreateWindow, CursorEntered, CursorLeft, CursorMoved, FileDragAndDrop, ModifiesWindows,
ReceivedCharacter, RequestRedraw, WindowBackendScaleFactorChanged, WindowCloseRequested,
WindowCreated, WindowFocused, WindowMoved, WindowResized, WindowScaleFactorChanged, Windows,
WindowClosed, WindowCreated, WindowFocused, WindowMoved, WindowResized,
WindowScaleFactorChanged, Windows,
};

use winit::{
dpi::PhysicalPosition,
dpi::{LogicalSize, PhysicalPosition},
event::{self, DeviceEvent, Event, StartCause, WindowEvent},
event_loop::{ControlFlow, EventLoop, EventLoopWindowTarget},
};

use winit::dpi::LogicalSize;

#[derive(Default)]
pub struct WinitPlugin;

Expand All @@ -51,10 +50,12 @@ impl Plugin for WinitPlugin {
}

fn change_window(
winit_windows: NonSend<WinitWindows>,
mut winit_windows: NonSendMut<WinitWindows>,
mut windows: ResMut<Windows>,
mut window_dpi_changed_events: EventWriter<WindowScaleFactorChanged>,
mut window_close_events: EventWriter<WindowClosed>,
) {
let mut removed_windows = vec![];
for bevy_window in windows.iter_mut() {
let id = bevy_window.id();
for command in bevy_window.drain_commands() {
Expand Down Expand Up @@ -166,9 +167,25 @@ fn change_window(
window.set_max_inner_size(Some(max_inner_size));
}
}
bevy_window::WindowCommand::Close => {
// Since we have borrowed `windows` to iterate through them, we can't remove the window from it.
// Add the removal requests to a queue to solve this
removed_windows.push(id);
// No need to run any further commands - this drops the rest of the commands, although the `bevy_window::Window` will be dropped later anyway
break;
}
}
}
}
if !removed_windows.is_empty() {
for id in removed_windows {
// Close the OS window. (The `Drop` impl actually closes the window)
let _ = winit_windows.remove_window(id);
// Clean up our own data structures
windows.remove(id);
window_close_events.send(WindowClosed { id });
}
}
}

fn run<F>(event_loop: EventLoop<()>, event_handler: F) -> !
Expand Down Expand Up @@ -316,7 +333,8 @@ pub fn winit_runner_with(mut app: App) {
let window = if let Some(window) = windows.get_mut(window_id) {
window
} else {
warn!("Skipped event for unknown Window Id {:?}", winit_window_id);
// If we're here, this window was previously opened
info!("Skipped event for closed window: {:?}", window_id);
return;
};
winit_state.low_power_event = true;
Expand Down
6 changes: 6 additions & 0 deletions crates/bevy_winit/src/winit_windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,12 @@ impl WinitWindows {
pub fn get_window_id(&self, id: winit::window::WindowId) -> Option<WindowId> {
self.winit_to_window_id.get(&id).cloned()
}

pub fn remove_window(&mut self, id: WindowId) -> Option<winit::window::Window> {
let winit_id = self.window_id_to_winit.remove(&id)?;
// Don't remove from winit_to_window_id, to track that we used to know about this winit window
self.windows.remove(&winit_id)
}
}

pub fn get_fitting_videomode(
Expand Down
Loading