diff --git a/crates/bevy_input/src/lib.rs b/crates/bevy_input/src/lib.rs index a9dbc2d3301d37..48041ff9412f89 100644 --- a/crates/bevy_input/src/lib.rs +++ b/crates/bevy_input/src/lib.rs @@ -3,7 +3,6 @@ pub mod gamepad; mod input; pub mod keyboard; pub mod mouse; -pub mod system; pub mod touch; pub use axis::*; diff --git a/crates/bevy_input/src/system.rs b/crates/bevy_input/src/system.rs deleted file mode 100644 index 018c47794b4e10..00000000000000 --- a/crates/bevy_input/src/system.rs +++ /dev/null @@ -1,20 +0,0 @@ -use crate::{ - keyboard::{KeyCode, KeyboardInput}, - ElementState, -}; -use bevy_app::AppExit; -use bevy_ecs::prelude::{EventReader, EventWriter}; - -/// Sends the `AppExit` event whenever the "esc" key is pressed. -pub fn exit_on_esc_system( - mut keyboard_input_events: EventReader, - mut app_exit_events: EventWriter, -) { - for event in keyboard_input_events.iter() { - if let Some(key_code) = event.key_code { - if event.state == ElementState::Pressed && key_code == KeyCode::Escape { - app_exit_events.send_default(); - } - } - } -} diff --git a/crates/bevy_pbr/src/light.rs b/crates/bevy_pbr/src/light.rs index 61f416913e370c..d0e87a4a0d8c28 100644 --- a/crates/bevy_pbr/src/light.rs +++ b/crates/bevy_pbr/src/light.rs @@ -371,9 +371,13 @@ pub fn add_clusters( pub fn update_clusters(windows: Res, mut views: Query<(&Camera, &mut Clusters)>) { for (camera, mut clusters) in views.iter_mut() { + // If the window doesn't exist, we won't render anything to it, so don't need to calculate the clusters for it + let window = match windows.get(camera.window) { + Some(window) => window, + _ => continue, + }; let is_orthographic = camera.projection_matrix.w_axis.w == 1.0; let inverse_projection = camera.projection_matrix.inverse(); - let window = windows.get(camera.window).unwrap(); let screen_size_u32 = UVec2::new(window.physical_width(), window.physical_height()); // Don't update clusters if screen size is 0. if screen_size_u32.x == 0 || screen_size_u32.y == 0 { diff --git a/crates/bevy_render/src/view/window.rs b/crates/bevy_render/src/view/window.rs index a120f44c8f822f..305a56ddd115bc 100644 --- a/crates/bevy_render/src/view/window.rs +++ b/crates/bevy_render/src/view/window.rs @@ -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; @@ -67,7 +67,11 @@ impl DerefMut for ExtractedWindows { } } -fn extract_windows(mut render_world: ResMut, windows: Res) { +fn extract_windows( + mut render_world: ResMut, + mut closed: EventReader, + windows: Res, +) { let mut extracted_windows = render_world.get_resource_mut::().unwrap(); for window in windows.iter() { let (new_width, new_height) = ( @@ -105,6 +109,9 @@ fn extract_windows(mut render_world: ResMut, windows: Res) extracted_window.physical_height = new_height; } } + for closed_window in closed.iter() { + extracted_windows.remove(&closed_window.id); + } } #[derive(Default)] diff --git a/crates/bevy_window/Cargo.toml b/crates/bevy_window/Cargo.toml index b64a69cde14fd5..f7f88f7175a20d 100644 --- a/crates/bevy_window/Cargo.toml +++ b/crates/bevy_window/Cargo.toml @@ -10,6 +10,9 @@ keywords = ["bevy"] [dependencies] # bevy +bevy_ecs = { path = "../bevy_ecs", version = "0.6.0" } +# Used for close_on_esc +bevy_input = { path = "../bevy_input", version = "0.6.0" } bevy_app = { path = "../bevy_app", version = "0.6.0" } bevy_math = { path = "../bevy_math", version = "0.6.0" } bevy_utils = { path = "../bevy_utils", version = "0.6.0" } diff --git a/crates/bevy_window/src/event.rs b/crates/bevy_window/src/event.rs index 72d6483d9b0dfe..a5726765c8a53d 100644 --- a/crates/bevy_window/src/event.rs +++ b/crates/bevy_window/src/event.rs @@ -20,12 +20,6 @@ pub struct CreateWindow { pub descriptor: WindowDescriptor, } -/// An event that indicates a window should be closed. -#[derive(Debug, Clone)] -pub struct CloseWindow { - pub id: WindowId, -} - /// An event that is sent whenever a new window is created. #[derive(Debug, Clone)] pub struct WindowCreated { @@ -34,11 +28,26 @@ pub struct WindowCreated { /// An event that is sent whenever a close was requested for a window. For example: when the "close" /// button is pressed on a window. +/// +/// By default, these events are handled by closing the corresponding [`crate::Window`]. +/// To disable this behaviour, set `close_when_requested` on the [`crate::WindowPlugin`] to `false` #[derive(Debug, Clone)] pub struct WindowCloseRequested { pub id: WindowId, } +/// An event that is sent whenever a window is closed. +/// This will only be sent in response to the [`Window::close`] method. +/// +/// By default, when no windows are open, the app will close. +/// To disable this behaviour, set `exit_on_all_closed` on the [`crate::WindowPlugin`] to `false` +/// +/// [`Window::close`]: crate::Window::close +#[derive(Debug, Clone)] +pub struct WindowClosed { + pub id: WindowId, +} + #[derive(Debug, Clone)] pub struct CursorMoved { pub id: WindowId, diff --git a/crates/bevy_window/src/lib.rs b/crates/bevy_window/src/lib.rs index a63ca57a96b7d1..08749f62838494 100644 --- a/crates/bevy_window/src/lib.rs +++ b/crates/bevy_window/src/lib.rs @@ -24,14 +24,25 @@ use bevy_app::{prelude::*, Events}; pub struct WindowPlugin { pub add_primary_window: bool, - pub exit_on_close: bool, + /// Whether to close the app when there are no open windows. + /// If disabling this, consider ensuring that you send a [`bevy_app::AppExit`] event yourself + /// when the app should exit; otherwise you will create headless processes, which would be + /// surprising for your users. + /// + /// This setting controls whether this plugin adds [`exit_on_all_closed`] + 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) + /// + /// This setting controls whether this plugin adds [`close_when_requested`] + pub close_when_requested: bool, } 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, } } } @@ -41,8 +52,8 @@ impl Plugin for WindowPlugin { app.add_event::() .add_event::() .add_event::() + .add_event::() .add_event::() - .add_event::() .add_event::() .add_event::() .add_event::() @@ -70,8 +81,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); } } } diff --git a/crates/bevy_window/src/system.rs b/crates/bevy_window/src/system.rs index 1985f7db574d03..1ec6faa8a4f9e5 100644 --- a/crates/bevy_window/src/system.rs +++ b/crates/bevy_window/src/system.rs @@ -1,11 +1,55 @@ -use crate::WindowCloseRequested; +use crate::{Window, WindowCloseRequested, WindowFocused, WindowId, Windows}; use bevy_app::{AppExit, EventReader, EventWriter}; +use bevy_ecs::prelude::*; +use bevy_input::{keyboard::KeyCode, Input}; -pub fn exit_on_window_close_system( - mut app_exit_events: EventWriter, - mut window_close_requested_events: EventReader, -) { - if window_close_requested_events.iter().next().is_some() { +/// Whether to exit the application when there are no open windows. +/// +/// By default, this system is added by the [`crate::WindowPlugin`]. +/// To disable this behaviour, set `close_when_requested` (on the [`crate::WindowPlugin`]) to `false`. +/// Please ensure that you read the caveats documented on that field. + +pub fn exit_on_all_closed(mut app_exit_events: EventWriter, windows: Res) { + if windows.iter().count() == 0 { app_exit_events.send(AppExit); } } + +/// Whether to close windows when they are requested to be closed (i.e. when the close button is pressed). +/// Not adding this system (without replacement) will lead to the close button having no effect. +/// +/// By default, this system is added by the [`crate::WindowPlugin`]. +/// To disable this behaviour, set `close_when_requested` (on the [`crate::WindowPlugin`]) to `false` +pub fn close_when_requested( + mut windows: ResMut, + mut closed: EventReader, +) { + for event in closed.iter() { + windows.get_mut(event.id).map(Window::close); + } +} + +// TODO: Consider using the kbd tag here for escape: esc +// Currently, it isn't rendered by vscode's hover markdown provider (and the contents are lost) +/// Close the focused window whenever the escape key is pressed +/// +/// This is useful for examples +pub fn close_on_esc( + mut focused: Local>, + mut focused_events: EventReader, + mut windows: ResMut, + input: Res>, +) { + // 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(); + } + } + } +} diff --git a/crates/bevy_window/src/window.rs b/crates/bevy_window/src/window.rs index 47c03d085ce7ef..801f46fe5d7585 100644 --- a/crates/bevy_window/src/window.rs +++ b/crates/bevy_window/src/window.rs @@ -219,6 +219,7 @@ pub enum WindowCommand { SetResizeConstraints { resize_constraints: WindowResizeConstraints, }, + Close, } /// Defines the way a window is displayed @@ -571,6 +572,10 @@ impl Window { }); } + pub fn close(&mut self) { + self.command_queue.push(WindowCommand::Close); + } + #[inline] pub fn drain_commands(&mut self) -> impl Iterator + '_ { self.command_queue.drain(..) diff --git a/crates/bevy_window/src/windows.rs b/crates/bevy_window/src/windows.rs index a669799b572f95..c1450acc86f7f3 100644 --- a/crates/bevy_window/src/windows.rs +++ b/crates/bevy_window/src/windows.rs @@ -34,4 +34,8 @@ impl Windows { pub fn iter_mut(&mut self) -> impl Iterator { self.windows.values_mut() } + + pub fn remove(&mut self, id: WindowId) -> Option { + self.windows.remove(&id) + } } diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index c58c9e4fc8906e..5cd511135d6592 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -13,11 +13,11 @@ pub use winit_windows::*; use bevy_app::{App, AppExit, CoreStage, Events, ManualEventReader, Plugin}; use bevy_ecs::{system::IntoExclusiveSystem, world::World}; use bevy_math::{ivec2, DVec2, Vec2}; -use bevy_utils::tracing::{error, trace, warn}; +use bevy_utils::tracing::{error, info, trace, warn}; use bevy_window::{ CreateWindow, CursorEntered, CursorLeft, CursorMoved, FileDragAndDrop, ReceivedCharacter, - WindowBackendScaleFactorChanged, WindowCloseRequested, WindowCreated, WindowFocused, - WindowMoved, WindowResized, WindowScaleFactorChanged, Windows, + WindowBackendScaleFactorChanged, WindowCloseRequested, WindowClosed, WindowCreated, + WindowFocused, WindowMoved, WindowResized, WindowScaleFactorChanged, Windows, }; use winit::{ dpi::PhysicalPosition, @@ -43,8 +43,9 @@ impl Plugin for WinitPlugin { fn change_window(world: &mut World) { let world = world.cell(); - let winit_windows = world.get_resource::().unwrap(); + let mut winit_windows = world.get_resource_mut::().unwrap(); let mut windows = world.get_resource_mut::().unwrap(); + let mut removed_windows = Vec::new(); for bevy_window in windows.iter_mut() { let id = bevy_window.id(); @@ -160,9 +161,26 @@ fn change_window(world: &mut World) { window.set_max_inner_size(Some(max_inner_size)); } } + bevy_window::WindowCommand::Close => { + let window = winit_windows.remove_window(id); + // Close the window + drop(window); + // Since we borrow `windows` here to iterate through them, we can't mutate it here. + // Add it to the 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() { + let mut events = world.get_resource_mut::>().unwrap(); + for id in removed_windows { + windows.remove(id); + events.send(WindowClosed { id }); + } + } } fn run(event_loop: EventLoop<()>, event_handler: F) -> ! @@ -280,7 +298,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; }; diff --git a/crates/bevy_winit/src/winit_windows.rs b/crates/bevy_winit/src/winit_windows.rs index f3fd83c6e84203..27f772b444e426 100644 --- a/crates/bevy_winit/src/winit_windows.rs +++ b/crates/bevy_winit/src/winit_windows.rs @@ -184,6 +184,12 @@ impl WinitWindows { pub fn get_window_id(&self, id: winit::window::WindowId) -> Option { self.winit_to_window_id.get(&id).cloned() } + + pub fn remove_window(&mut self, id: WindowId) -> Option { + 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( diff --git a/examples/2d/rotation.rs b/examples/2d/rotation.rs index 9ca891fd116c91..79622858390112 100644 --- a/examples/2d/rotation.rs +++ b/examples/2d/rotation.rs @@ -18,7 +18,7 @@ fn main() { .with_system(snap_to_player_system) .with_system(rotate_to_player_system), ) - .add_system(bevy::input::system::exit_on_esc_system) + .add_system(bevy::window::close_on_esc) .run(); } diff --git a/examples/game/alien_cake_addict.rs b/examples/game/alien_cake_addict.rs index 683d085c5f3f10..02fc66873a7d66 100644 --- a/examples/game/alien_cake_addict.rs +++ b/examples/game/alien_cake_addict.rs @@ -1,5 +1,6 @@ use bevy::{ core::FixedTimestep, ecs::schedule::SystemSet, prelude::*, render::camera::CameraPlugin, + window::close_on_esc, }; use rand::Rng; @@ -33,7 +34,7 @@ fn main() { .with_run_criteria(FixedTimestep::step(5.0)) .with_system(spawn_bonus), ) - .add_system(bevy::input::system::exit_on_esc_system) + .add_system(close_on_esc) .run(); } diff --git a/examples/game/breakout.rs b/examples/game/breakout.rs index 66246feecc6204..a5b440bb738c5c 100644 --- a/examples/game/breakout.rs +++ b/examples/game/breakout.rs @@ -2,6 +2,7 @@ use bevy::{ core::FixedTimestep, prelude::*, sprite::collide_aabb::{collide, Collision}, + window::close_on_esc, }; /// An implementation of the classic game "Breakout" @@ -20,7 +21,7 @@ fn main() { .with_system(ball_movement_system), ) .add_system(scoreboard_system) - .add_system(bevy::input::system::exit_on_esc_system) + .add_system(close_on_esc) .run(); } diff --git a/examples/window/multiple_windows.rs b/examples/window/multiple_windows.rs index b84d3eaec7f371..d17af7a3117b71 100644 --- a/examples/window/multiple_windows.rs +++ b/examples/window/multiple_windows.rs @@ -8,7 +8,7 @@ use bevy::{ renderer::RenderContext, RenderApp, RenderStage, }, - window::{CreateWindow, PresentMode, WindowId}, + window::{close_on_esc, CreateWindow, PresentMode, WindowId}, }; /// This example creates a second window and draws a mesh from two different cameras, one in each window @@ -16,7 +16,8 @@ fn main() { let mut app = App::new(); app.add_plugins(DefaultPlugins) .add_startup_system(setup) - .add_startup_system(create_new_window); + .add_startup_system(create_new_window) + .add_system(close_on_esc); let render_app = app.sub_app_mut(RenderApp); render_app.add_system_to_stage(RenderStage::Extract, extract_secondary_camera_phases);